| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| node-npmtest-shaka-player/ | 100% | (153 / 153) | 100% | (126 / 126) | 100% | (28 / 28) | 100% | (153 / 153) | |
| node-npmtest-shaka-player/node_modules/shaka-player/ | 7.41% | (6 / 81) | 0% | (0 / 26) | 0% | (0 / 6) | 7.41% | (6 / 81) | |
| node-npmtest-shaka-player/node_modules/shaka-player/demo/ | 11.99% | (80 / 667) | 3.81% | (11 / 289) | 1.18% | (1 / 85) | 12.16% | (80 / 658) | |
| node-npmtest-shaka-player/node_modules/shaka-player/dist/ | 18.69% | (810 / 4335) | 0.48% | (13 / 2734) | 0.62% | (6 / 971) | 92.96% | (251 / 270) | |
| node-npmtest-shaka-player/node_modules/shaka-player/externs/ | 43.33% | (65 / 150) | 100% | (0 / 0) | 0% | (0 / 74) | 43.33% | (65 / 150) | |
| node-npmtest-shaka-player/node_modules/shaka-player/externs/shaka/ | 14.55% | (8 / 55) | 100% | (0 / 0) | 0% | (0 / 13) | 14.55% | (8 / 55) | |
| node-npmtest-shaka-player/node_modules/shaka-player/lib/ | 0.19% | (1 / 537) | 0% | (0 / 210) | 0% | (0 / 84) | 0.19% | (1 / 528) | |
| node-npmtest-shaka-player/node_modules/shaka-player/lib/abr/ | 1.88% | (3 / 160) | 0% | (0 / 47) | 0% | (0 / 24) | 1.89% | (3 / 159) | |
| node-npmtest-shaka-player/node_modules/shaka-player/lib/cast/ | 0.66% | (4 / 606) | 0% | (0 / 203) | 0% | (0 / 99) | 0.67% | (4 / 601) | |
| node-npmtest-shaka-player/node_modules/shaka-player/lib/dash/ | 0.58% | (6 / 1040) | 0% | (0 / 486) | 0% | (0 / 114) | 0.58% | (6 / 1032) | |
| node-npmtest-shaka-player/node_modules/shaka-player/lib/debug/ | 4.26% | (2 / 47) | 0% | (0 / 22) | 0% | (0 / 8) | 4.26% | (2 / 47) | |
| node-npmtest-shaka-player/node_modules/shaka-player/lib/media/ | 0.71% | (17 / 2396) | 0% | (0 / 1032) | 0% | (0 / 287) | 0.73% | (17 / 2327) | |
| node-npmtest-shaka-player/node_modules/shaka-player/lib/net/ | 1.63% | (3 / 184) | 0% | (0 / 55) | 0% | (0 / 28) | 1.63% | (3 / 184) | |
| node-npmtest-shaka-player/node_modules/shaka-player/lib/offline/ | 1.2% | (7 / 585) | 0% | (0 / 133) | 0% | (0 / 144) | 1.23% | (7 / 571) | |
| node-npmtest-shaka-player/node_modules/shaka-player/lib/polyfill/ | 1.19% | (12 / 1011) | 0% | (0 / 305) | 0% | (0 / 129) | 1.19% | (12 / 1008) | |
| node-npmtest-shaka-player/node_modules/shaka-player/lib/util/ | 2.56% | (23 / 897) | 0% | (0 / 389) | 0% | (0 / 151) | 2.61% | (23 / 880) | |
| node-npmtest-shaka-player/node_modules/shaka-player/third_party/closure/goog/ | 28.34% | (53 / 187) | 23.58% | (25 / 106) | 12% | (3 / 25) | 28.34% | (53 / 187) | |
| node-npmtest-shaka-player/node_modules/shaka-player/third_party/jsdoc/ | 6.32% | (16 / 253) | 3.19% | (3 / 94) | 6.06% | (2 / 33) | 6.32% | (16 / 253) | |
| node-npmtest-shaka-player/node_modules/shaka-player/third_party/jsdoc/node/ | 11.11% | (4 / 36) | 0% | (0 / 6) | 0% | (0 / 4) | 11.11% | (4 / 36) | |
| node-npmtest-shaka-player/node_modules/shaka-player/third_party/jsdoc/plugins/ | 13.3% | (27 / 203) | 1.67% | (2 / 120) | 0% | (0 / 41) | 13.3% | (27 / 203) |
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| example.js | 100% | (83 / 83) | 100% | (73 / 73) | 100% | (12 / 12) | 100% | (83 / 83) | |
| lib.npmtest_shaka_player.js | 100% | (16 / 16) | 100% | (14 / 14) | 100% | (3 / 3) | 100% | (16 / 16) | |
| test.js | 100% | (54 / 54) | 100% | (39 / 39) | 100% | (13 / 13) | 100% | (54 / 54) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | 2 2 2 2 2 2 2 1 2 2 2 2 1 2 2 2 2 2 1 2 1 1 1 1 1 1 1 1 1 2 1 1 1 1 2 2 3 3 3 3 1 3 3 3 1 3 1 1 1 1 1 1 1 1 1 1 1 1 6 6 1 2 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | /*
example.js
quickstart example
instruction
1. save this script as example.js
2. run the shell command:
$ npm install npmtest-shaka-player && PORT=8081 node example.js
3. play with the browser-demo on http://127.0.0.1:8081
*/
/* istanbul instrument in package npmtest_shaka_player */
/*jslint
bitwise: true,
browser: true,
maxerr: 8,
maxlen: 96,
node: true,
nomen: true,
regexp: true,
stupid: true
*/
(function () {
'use strict';
var local;
// run shared js-env code - pre-init
(function () {
// init local
local = {};
// init modeJs
local.modeJs = (function () {
try {
return typeof navigator.userAgent === 'string' &&
typeof document.querySelector('body') === 'object' &&
typeof XMLHttpRequest.prototype.open === 'function' &&
'browser';
} catch (errorCaughtBrowser) {
return module.exports &&
typeof process.versions.node === 'string' &&
typeof require('http').createServer === 'function' &&
'node';
}
}());
// init global
local.global = local.modeJs === 'browser'
? window
: global;
// init utility2_rollup
local = local.global.utility2_rollup || (local.modeJs === 'browser'
? local.global.utility2_npmtest_shaka_player
: global.utility2_moduleExports);
// export local
local.global.local = local;
}());
switch (local.modeJs) {
// post-init
// run browser js-env code - post-init
/* istanbul ignore next */
case 'browser':
local.testRunBrowser = function (event) {
Eif (!event || (event &&
event.currentTarget &&
event.currentTarget.className &&
event.currentTarget.className.includes &&
event.currentTarget.className.includes('onreset'))) {
// reset output
Array.from(
document.querySelectorAll('body > .resettable')
).forEach(function (element) {
switch (element.tagName) {
case 'INPUT':
case 'TEXTAREA':
element.value = '';
break;
default:
element.textContent = '';
}
});
}
switch (event && event.currentTarget && event.currentTarget.id) {
case 'testRunButton1':
// show tests
Eif (document.querySelector('#testReportDiv1').style.display === 'none') {
document.querySelector('#testReportDiv1').style.display = 'block';
document.querySelector('#testRunButton1').textContent =
'hide internal test';
local.modeTest = true;
local.testRunDefault(local);
// hide tests
} else {
document.querySelector('#testReportDiv1').style.display = 'none';
document.querySelector('#testRunButton1').textContent = 'run internal test';
}
break;
// custom-case
default:
break;
}
Iif (document.querySelector('#inputTextareaEval1') && (!event || (event &&
event.currentTarget &&
event.currentTarget.className &&
event.currentTarget.className.includes &&
event.currentTarget.className.includes('oneval')))) {
// try to eval input-code
try {
/*jslint evil: true*/
eval(document.querySelector('#inputTextareaEval1').value);
} catch (errorCaught) {
console.error(errorCaught);
}
}
};
// log stderr and stdout to #outputTextareaStdout1
['error', 'log'].forEach(function (key) {
console[key + '_original'] = console[key];
console[key] = function () {
var element;
console[key + '_original'].apply(console, arguments);
element = document.querySelector('#outputTextareaStdout1');
Iif (!element) {
return;
}
// append text to #outputTextareaStdout1
element.value += Array.from(arguments).map(function (arg) {
return typeof arg === 'string'
? arg
: JSON.stringify(arg, null, 4);
}).join(' ') + '\n';
// scroll textarea to bottom
element.scrollTop = element.scrollHeight;
};
});
// init event-handling
['change', 'click', 'keyup'].forEach(function (event) {
Array.from(document.querySelectorAll('.on' + event)).forEach(function (element) {
element.addEventListener(event, local.testRunBrowser);
});
});
// run tests
local.testRunBrowser();
break;
// run node js-env code - post-init
/* istanbul ignore next */
case 'node':
// export local
module.exports = local;
// require modules
local.fs = require('fs');
local.http = require('http');
local.url = require('url');
// init assets
local.assetsDict = local.assetsDict || {};
/* jslint-ignore-begin */
local.assetsDict['/assets.index.template.html'] = '\
<!doctype html>\n\
<html lang="en">\n\
<head>\n\
<meta charset="UTF-8">\n\
<meta name="viewport" content="width=device-width, initial-scale=1">\n\
<title>{{env.npm_package_name}} (v{{env.npm_package_version}})</title>\n\
<style>\n\
/*csslint\n\
box-sizing: false,\n\
universal-selector: false\n\
*/\n\
* {\n\
box-sizing: border-box;\n\
}\n\
body {\n\
background: #dde;\n\
font-family: Arial, Helvetica, sans-serif;\n\
margin: 2rem;\n\
}\n\
body > * {\n\
margin-bottom: 1rem;\n\
}\n\
.utility2FooterDiv {\n\
margin-top: 20px;\n\
text-align: center;\n\
}\n\
</style>\n\
<style>\n\
/*csslint\n\
*/\n\
textarea {\n\
font-family: monospace;\n\
height: 10rem;\n\
width: 100%;\n\
}\n\
textarea[readonly] {\n\
background: #ddd;\n\
}\n\
</style>\n\
</head>\n\
<body>\n\
<!-- utility2-comment\n\
<div id="ajaxProgressDiv1" style="background: #d00; height: 2px; left: 0; margin: 0; padding: 0; position: fixed; top: 0; transition: background 0.5s, width 1.5s; width: 25%;"></div>\n\
utility2-comment -->\n\
<h1>\n\
<!-- utility2-comment\n\
<a\n\
{{#if env.npm_package_homepage}}\n\
href="{{env.npm_package_homepage}}"\n\
{{/if env.npm_package_homepage}}\n\
target="_blank"\n\
>\n\
utility2-comment -->\n\
{{env.npm_package_name}} (v{{env.npm_package_version}})\n\
<!-- utility2-comment\n\
</a>\n\
utility2-comment -->\n\
</h1>\n\
<h3>{{env.npm_package_description}}</h3>\n\
<!-- utility2-comment\n\
<h4><a download href="assets.app.js">download standalone app</a></h4>\n\
<button class="onclick onreset" id="testRunButton1">run internal test</button><br>\n\
<div id="testReportDiv1" style="display: none;"></div>\n\
utility2-comment -->\n\
\n\
\n\
\n\
<label>stderr and stdout</label>\n\
<textarea class="resettable" id="outputTextareaStdout1" readonly></textarea>\n\
<!-- utility2-comment\n\
{{#if isRollup}}\n\
<script src="assets.app.js"></script>\n\
{{#unless isRollup}}\n\
utility2-comment -->\n\
<script src="assets.utility2.rollup.js"></script>\n\
<script src="jsonp.utility2._stateInit?callback=window.utility2._stateInit"></script>\n\
<script src="assets.npmtest_shaka_player.rollup.js"></script>\n\
<script src="assets.example.js"></script>\n\
<script src="assets.test.js"></script>\n\
<!-- utility2-comment\n\
{{/if isRollup}}\n\
utility2-comment -->\n\
<div class="utility2FooterDiv">\n\
[ this app was created with\n\
<a href="https://github.com/kaizhu256/node-utility2" target="_blank">utility2</a>\n\
]\n\
</div>\n\
</body>\n\
</html>\n\
';
/* jslint-ignore-end */
Iif (local.templateRender) {
local.assetsDict['/'] = local.templateRender(
local.assetsDict['/assets.index.template.html'],
{
env: local.objectSetDefault(local.env, {
npm_package_description: 'the greatest app in the world!',
npm_package_name: 'my-app',
npm_package_nameAlias: 'my_app',
npm_package_version: '0.0.1'
})
}
);
} else {
local.assetsDict['/'] = local.assetsDict['/assets.index.template.html']
.replace((/\{\{env\.(\w+?)\}\}/g), function (match0, match1) {
// jslint-hack
String(match0);
switch (match1) {
case 'npm_package_description':
return 'the greatest app in the world!';
case 'npm_package_name':
return 'my-app';
case 'npm_package_nameAlias':
return 'my_app';
case 'npm_package_version':
return '0.0.1';
}
});
}
// run the cli
Eif (local.global.utility2_rollup || module !== require.main) {
break;
}
local.assetsDict['/assets.example.js'] =
local.assetsDict['/assets.example.js'] ||
local.fs.readFileSync(__filename, 'utf8');
// bug-workaround - long $npm_package_buildCustomOrg
/* jslint-ignore-begin */
local.assetsDict['/assets.npmtest_shaka_player.rollup.js'] =
local.assetsDict['/assets.npmtest_shaka_player.rollup.js'] ||
local.fs.readFileSync(
local.npmtest_shaka_player.__dirname + '/lib.npmtest_shaka_player.js',
'utf8'
).replace((/^#!/), '//');
/* jslint-ignore-end */
local.assetsDict['/favicon.ico'] = local.assetsDict['/favicon.ico'] || '';
// if $npm_config_timeout_exit exists,
// then exit this process after $npm_config_timeout_exit ms
if (Number(process.env.npm_config_timeout_exit)) {
setTimeout(process.exit, Number(process.env.npm_config_timeout_exit));
}
// start server
if (local.global.utility2_serverHttp1) {
break;
}
process.env.PORT = process.env.PORT || '8081';
console.error('server starting on port ' + process.env.PORT);
local.http.createServer(function (request, response) {
request.urlParsed = local.url.parse(request.url);
if (local.assetsDict[request.urlParsed.pathname] !== undefined) {
response.end(local.assetsDict[request.urlParsed.pathname]);
return;
}
response.statusCode = 404;
response.end();
}).listen(process.env.PORT);
break;
}
}());
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | 2 2 2 2 2 2 2 1 2 2 2 2 1 1 1 1 | /* istanbul instrument in package npmtest_shaka_player */
/*jslint
bitwise: true,
browser: true,
maxerr: 8,
maxlen: 96,
node: true,
nomen: true,
regexp: true,
stupid: true
*/
(function () {
'use strict';
var local;
// run shared js-env code - pre-init
(function () {
// init local
local = {};
// init modeJs
local.modeJs = (function () {
try {
return typeof navigator.userAgent === 'string' &&
typeof document.querySelector('body') === 'object' &&
typeof XMLHttpRequest.prototype.open === 'function' &&
'browser';
} catch (errorCaughtBrowser) {
return module.exports &&
typeof process.versions.node === 'string' &&
typeof require('http').createServer === 'function' &&
'node';
}
}());
// init global
local.global = local.modeJs === 'browser'
? window
: global;
// init utility2_rollup
local = local.global.utility2_rollup || local;
// init lib
local.local = local.npmtest_shaka_player = local;
// init exports
if (local.modeJs === 'browser') {
local.global.utility2_npmtest_shaka_player = local;
} else {
module.exports = local;
module.exports.__dirname = __dirname;
module.exports.module = module;
}
}());
}());
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | 2 2 2 2 2 2 2 1 2 2 1 1 1 1 2 2 2 2 1 1 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 1 2 2 1 2 2 1 2 2 1 1 1 1 1 | /* istanbul instrument in package npmtest_shaka_player */
/*jslint
bitwise: true,
browser: true,
maxerr: 8,
maxlen: 96,
node: true,
nomen: true,
regexp: true,
stupid: true
*/
(function () {
'use strict';
var local;
// run shared js-env code - pre-init
(function () {
// init local
local = {};
// init modeJs
local.modeJs = (function () {
try {
return typeof navigator.userAgent === 'string' &&
typeof document.querySelector('body') === 'object' &&
typeof XMLHttpRequest.prototype.open === 'function' &&
'browser';
} catch (errorCaughtBrowser) {
return module.exports &&
typeof process.versions.node === 'string' &&
typeof require('http').createServer === 'function' &&
'node';
}
}());
// init global
local.global = local.modeJs === 'browser'
? window
: global;
switch (local.modeJs) {
// re-init local from window.local
case 'browser':
local = local.global.utility2.objectSetDefault(
local.global.utility2_rollup || local.global.local,
local.global.utility2
);
break;
// re-init local from example.js
case 'node':
local = (local.global.utility2_rollup || require('utility2'))
.requireReadme();
break;
}
// export local
local.global.local = local;
}());
// run shared js-env code - function
(function () {
return;
}());
switch (local.modeJs) {
// run browser js-env code - function
case 'browser':
break;
// run node js-env code - function
case 'node':
break;
}
// run shared js-env code - post-init
(function () {
return;
}());
switch (local.modeJs) {
// run browser js-env code - post-init
case 'browser':
local.testCase_browser_nullCase = local.testCase_browser_nullCase || function (
options,
onError
) {
/*
* this function will test browsers's null-case handling-behavior-behavior
*/
onError(null, options);
};
// run tests
local.nop(local.modeTest &&
document.querySelector('#testRunButton1') &&
document.querySelector('#testRunButton1').click());
break;
// run node js-env code - post-init
/* istanbul ignore next */
case 'node':
local.testCase_buildApidoc_default = local.testCase_buildApidoc_default || function (
options,
onError
) {
/*
* this function will test buildApidoc's default handling-behavior-behavior
*/
options = { modulePathList: module.paths };
local.buildApidoc(options, onError);
};
local.testCase_buildApp_default = local.testCase_buildApp_default || function (
options,
onError
) {
/*
* this function will test buildApp's default handling-behavior-behavior
*/
local.testCase_buildReadme_default(options, local.onErrorThrow);
local.testCase_buildLib_default(options, local.onErrorThrow);
local.testCase_buildTest_default(options, local.onErrorThrow);
local.testCase_buildCustomOrg_default(options, local.onErrorThrow);
options = [];
local.buildApp(options, onError);
};
local.testCase_buildCustomOrg_default = local.testCase_buildCustomOrg_default ||
function (options, onError) {
/*
* this function will test buildCustomOrg's default handling-behavior
*/
options = {};
local.buildCustomOrg(options, onError);
};
local.testCase_buildLib_default = local.testCase_buildLib_default || function (
options,
onError
) {
/*
* this function will test buildLib's default handling-behavior
*/
options = {};
local.buildLib(options, onError);
};
local.testCase_buildReadme_default = local.testCase_buildReadme_default || function (
options,
onError
) {
/*
* this function will test buildReadme's default handling-behavior-behavior
*/
options = {};
local.buildReadme(options, onError);
};
local.testCase_buildTest_default = local.testCase_buildTest_default || function (
options,
onError
) {
/*
* this function will test buildTest's default handling-behavior
*/
options = {};
local.buildTest(options, onError);
};
local.testCase_webpage_default = local.testCase_webpage_default || function (
options,
onError
) {
/*
* this function will test webpage's default handling-behavior
*/
options = { modeCoverageMerge: true, url: local.serverLocalHost + '?modeTest=1' };
local.browserTest(options, onError);
};
// run test-server
local.testRunServer(local);
break;
}
}());
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| karma.conf.js | 9.8% | (5 / 51) | 0% | (0 / 26) | 0% | (0 / 6) | 9.8% | (5 / 51) | |
| shaka-player.uncompiled.js | 3.33% | (1 / 30) | 100% | (0 / 0) | 100% | (0 / 0) | 3.33% | (1 / 30) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | 1 1 1 1 1 | /** * @license * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Karma configuration // Install required modules by running "npm install" module.exports = function(config) { config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) basePath: '.', // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter frameworks: [ 'jasmine-ajax', 'jasmine', 'sprintf-js', ], plugins: [ 'karma-*', // default frameworkPluginForModule('sprintf-js'), ], // list of files / patterns to load in the browser files: [ // closure base first 'third_party/closure/goog/base.js', // deps next 'dist/deps.js', 'shaka-player.uncompiled.js', // requirejs next 'node_modules/requirejs/require.js', // bootstrapping for the test suite 'test/test/boot.js', // test utils next 'test/test/util/*.js', // list of test assets next 'demo/assets.js', // unit tests last 'test/**/*_unit.js', // if --quick is not present, we will add integration tests. // source files - these are only watched and served {pattern: 'lib/**/*.js', included: false}, {pattern: 'third_party/closure/goog/**/*.js', included: false}, {pattern: 'test/test/assets/*', included: false}, {pattern: 'dist/shaka-player.compiled.js', included: false}, ], // NOTE: Do not use proxies at all! They cannot be used with the --hostname // option, which is necessary for some of our lab testing. proxies: {}, preprocessors: { // Don't compute coverage over lib/debug/ or lib/polyfill/ 'lib/!(debug|polyfill)/*.js': 'coverage', // Player is not matched by the above, so add it explicitly 'lib/player.js': 'coverage', }, // to avoid DISCONNECTED messages on Safari: browserDisconnectTimeout: 10 * 1000, // 10s to reconnect browserDisconnectTolerance: 1, // max of 1 disconnect is OK browserNoActivityTimeout: 5 * 60 * 1000, // disconnect after 5m silence captureTimeout: 1 * 60 * 1000, // give up if startup takes 1m // https://support.saucelabs.com/customer/en/portal/articles/2440724 client: { // don't capture the client's console logs captureConsole: false, // |args| must be an array; pass a key-value map as the sole client // argument. args: [{}], }, // enable / disable colors in the output (reporters and logs) colors: true, // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG logLevel: config.LOG_WARN, // do not execute tests whenever any file changes autoWatch: false, // do a single run of the tests on captured browsers and then quit singleRun: true, coverageReporter: { includeAllSources: true, reporters: [ { type: 'text' }, ], }, specReporter: { suppressSkipped: true, }, }); if (flagPresent('html-coverage-report')) { // Wipe out any old coverage reports to avoid confusion. var rimraf = require('rimraf'); rimraf.sync('coverage', {}); // Like rm -rf config.set({ reporters: [ 'coverage', 'progress' ], coverageReporter: { reporters: [ { type: 'html', dir: 'coverage' }, { type: 'cobertura', dir: 'coverage', file: 'coverage.xml' }, ], }, }); } if (!flagPresent('quick')) { // If --quick is present, we don't serve integration tests. var files = config.files; files.push('test/**/*_integration.js'); // We just modified the config in-place. No need for config.set(). } var logLevel = getFlagValue('enable-logging'); if (logLevel !== null) { if (logLevel === '') logLevel = 3; // INFO config.set({ reporters: ['spec'], }); // Setting |config.client| using config.set will remove the // |config.client.args| member. config.client.captureConsole = true; setClientArg(config, 'logLevel', logLevel); } if (flagPresent('external')) { // Run Player integration tests against external assets. // Skipped by default. setClientArg(config, 'external', true); } if (flagPresent('quarantined')) { // Run quarantined tests which do not consistently pass. // Skipped by default. setClientArg(config, 'quarantined', true); } if (flagPresent('uncompiled')) { // Run Player integration tests with uncompiled code for debugging. setClientArg(config, 'uncompiled', true); } if (flagPresent('random')) { // Run tests in a random order. setClientArg(config, 'random', true); // If --seed was specified use that value, else generate a seed so that the // exact order can be reproduced if it catches an issue. var seed = getFlagValue('seed') || new Date().getTime(); setClientArg(config, 'seed', seed); console.log("Using a random test order (--random) with --seed=" + seed); } if (flagPresent('specFilter')) { setClientArg(config, 'specFilter', getFlagValue('specFilter')); } }; // Sets the value of an argument passed to the client. function setClientArg(config, name, value) { config.client.args[0][name] = value; } // Find a custom command-line flag that has a value (e.g. --option=12). // Returns: // * string value --option=12 // * empty string --option= or --option // * null not present function getFlagValue(name) { var re = /^--([^=]+)(?:=(.*))?$/; for (var i = 0; i < process.argv.length; i++) { var match = re.exec(process.argv[i]); if (match && match[1] == name) { if (match[2] !== undefined) return match[2]; else return ''; } } return null; } // Find custom command-line flags. function flagPresent(name) { return getFlagValue(name) !== null; } // Construct framework plugins on-the-fly for arbitrary node modules. // A call to this must be placed in the config in the 'plugins' array, // and the module name must be added to the config in the 'frameworks' array. function frameworkPluginForModule(name) { // The framework injects files into the client which runs the tests. var framework = function(files) { // Locate the main file for the node module. var path = require('path'); var mainFile = path.resolve(require.resolve(name)); // Add a file entry to the list of files to be served. // This follows the same syntax as above in config.set({files: ...}). files.unshift({ pattern: mainFile, included: true, served: true, watched: false }); }; // The framework factory function takes one argument, which is the list of // files from the karma config. framework.$inject = ['config.files']; // This is the plugin interface to register a new framework. Adding this to // the list of plugins makes the named module available as a framework. That // framework then injects the module into the client. var obj = {}; obj['framework:' + name] = ['factory', framework]; return obj; } |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Require all exported classes an app might use.
* @suppress {extraRequire}
*/
goog.require('shaka.Player');
goog.require('shaka.abr.SimpleAbrManager');
goog.require('shaka.cast.CastProxy');
goog.require('shaka.cast.CastReceiver');
goog.require('shaka.dash.DashParser');
goog.require('shaka.log');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.ManifestParser');
goog.require('shaka.media.Mp4TtmlParser');
goog.require('shaka.media.Mp4VttParser');
goog.require('shaka.media.PresentationTimeline');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.media.TextEngine');
goog.require('shaka.media.TtmlTextParser');
goog.require('shaka.media.VttTextParser');
goog.require('shaka.net.DataUriPlugin');
goog.require('shaka.net.HttpPlugin');
goog.require('shaka.offline.OfflineManifestParser');
goog.require('shaka.offline.OfflineScheme');
goog.require('shaka.offline.Storage');
goog.require('shaka.polyfill.Fullscreen');
goog.require('shaka.polyfill.IndexedDB');
goog.require('shaka.polyfill.MediaKeys');
goog.require('shaka.polyfill.MediaSource');
goog.require('shaka.polyfill.Promise');
goog.require('shaka.polyfill.VTTCue');
goog.require('shaka.polyfill.VideoPlaybackQuality');
goog.require('shaka.polyfill.installAll');
goog.require('shaka.util.Error');
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| configuration_section.js | 35.71% | (5 / 14) | 50% | (2 / 4) | 0% | (0 / 4) | 38.46% | (5 / 13) | |
| controls.js | 11.99% | (32 / 267) | 0% | (0 / 125) | 0% | (0 / 36) | 12.08% | (32 / 265) | |
| demo_utils.js | 11.11% | (3 / 27) | 0% | (0 / 16) | 0% | (0 / 2) | 11.54% | (3 / 26) | |
| info_section.js | 8.82% | (6 / 68) | 12.5% | (2 / 16) | 0% | (0 / 12) | 9.23% | (6 / 65) | |
| load.js | 16.67% | (3 / 18) | 12.5% | (1 / 8) | 50% | (1 / 2) | 16.67% | (3 / 18) | |
| main.js | 12.75% | (13 / 102) | 6.52% | (3 / 46) | 0% | (0 / 6) | 12.87% | (13 / 101) | |
| offline_section.js | 8.65% | (9 / 104) | 3.7% | (2 / 54) | 0% | (0 / 14) | 8.65% | (9 / 104) | |
| receiver_app.js | 13.43% | (9 / 67) | 5% | (1 / 20) | 0% | (0 / 9) | 13.64% | (9 / 66) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | 1 1 1 1 1 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Shaka Player demo, main section.
*
* @suppress {visibility} to work around compiler errors until we can
* refactor the demo into classes that talk via public method. TODO
*/
/** @suppress {duplicate} */
var shakaDemo = shakaDemo || {};
/** @private */
shakaDemo.setupConfiguration_ = function() {
document.getElementById('preferredAudioLanguage').addEventListener(
'keyup', shakaDemo.onConfigKeyUp_);
document.getElementById('preferredTextLanguage').addEventListener(
'keyup', shakaDemo.onConfigKeyUp_);
document.getElementById('showTrickPlay').addEventListener(
'change', shakaDemo.onTrickPlayChange_);
document.getElementById('enableAdaptation').addEventListener(
'change', shakaDemo.onAdaptationChange_);
};
/**
* @param {!Event} event
* @private
*/
shakaDemo.onConfigKeyUp_ = function(event) {
// Update the configuration if the user presses enter.
if (event.keyCode != 13) return;
shakaDemo.player_.configure(/** @type {shakaExtern.PlayerConfiguration} */({
preferredAudioLanguage:
document.getElementById('preferredAudioLanguage').value,
preferredTextLanguage:
document.getElementById('preferredTextLanguage').value
}));
};
/**
* @param {!Event} event
* @private
*/
shakaDemo.onAdaptationChange_ = function(event) {
// Update adaptation config.
shakaDemo.player_.configure(/** @type {shakaExtern.PlayerConfiguration} */({
abr: { enabled: event.target.checked }
}));
};
/**
* @param {!Event} event
* @private
*/
shakaDemo.onTrickPlayChange_ = function(event) {
// Show/hide trick play controls.
shakaDemo.controls_.showTrickPlay(event.target.checked);
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 | 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* A container for custom video controls.
* @constructor
* @suppress {missingProvide}
*/
function ShakaControls() {
/** @private {shaka.cast.CastProxy} */
this.castProxy_ = null;
/** @private {boolean} */
this.castAllowed_ = true;
/** @private {?function(!shaka.util.Error)} */
this.onError_ = null;
/** @private {HTMLMediaElement} */
this.video_ = null;
/** @private {shaka.Player} */
this.player_ = null;
/** @private {Element} */
this.videoContainer_ = document.getElementById('videoContainer');
/** @private {Element} */
this.controls_ = document.getElementById('controls');
/** @private {Element} */
this.playPauseButton_ = document.getElementById('playPauseButton');
/** @private {Element} */
this.seekBar_ = document.getElementById('seekBar');
/** @private {Element} */
this.muteButton_ = document.getElementById('muteButton');
/** @private {Element} */
this.volumeBar_ = document.getElementById('volumeBar');
/** @private {Element} */
this.captionButton_ = document.getElementById('captionButton');
/** @private {Element} */
this.fullscreenButton_ = document.getElementById('fullscreenButton');
/** @private {Element} */
this.currentTime_ = document.getElementById('currentTime');
/** @private {Element} */
this.rewindButton_ = document.getElementById('rewindButton');
/** @private {Element} */
this.fastForwardButton_ = document.getElementById('fastForwardButton');
/** @private {Element} */
this.castButton_ = document.getElementById('castButton');
/** @private {Element} */
this.castReceiverName_ = document.getElementById('castReceiverName');
/** @private {Element} */
this.bufferingSpinner_ = document.getElementById('bufferingSpinner');
/** @private {Element} */
this.giantPlayButtonContainer_ =
document.getElementById('giantPlayButtonContainer');
/** @private {boolean} */
this.isSeeking_ = false;
/** @private {number} */
this.trickPlayRate_ = 1;
/** @private {?number} */
this.seekTimeoutId_ = null;
/** @private {?number} */
this.mouseStillTimeoutId_ = null;
/** @private {?number} */
this.lastTouchEventTime_ = null;
}
/**
* Initializes the player controls.
* @param {shaka.cast.CastProxy} castProxy
* @param {function(!shaka.util.Error)} onError
* @param {function(boolean)} notifyCastStatus
*/
ShakaControls.prototype.init = function(castProxy, onError, notifyCastStatus) {
this.castProxy_ = castProxy;
this.onError_ = onError;
this.notifyCastStatus_ = notifyCastStatus;
this.initMinimal(castProxy.getVideo(), castProxy.getPlayer());
// IE11 doesn't treat the 'input' event correctly.
// https://connect.microsoft.com/IE/Feedback/Details/856998
// If you know a better way than a userAgent check to handle this, please
// send a patch.
var sliderInputEvent = 'input';
// This matches IE11, but not Edge. Edge does not have this problem.
if (navigator.userAgent.indexOf('Trident/') >= 0) {
sliderInputEvent = 'change';
}
this.playPauseButton_.addEventListener(
'click', this.onPlayPauseClick_.bind(this));
this.video_.addEventListener(
'play', this.onPlayStateChange_.bind(this));
this.video_.addEventListener(
'pause', this.onPlayStateChange_.bind(this));
this.seekBar_.addEventListener(
'mousedown', this.onSeekStart_.bind(this));
this.seekBar_.addEventListener(
'touchstart', this.onSeekStart_.bind(this));
this.seekBar_.addEventListener(
sliderInputEvent, this.onSeekInput_.bind(this));
this.seekBar_.addEventListener(
'touchend', this.onSeekEnd_.bind(this));
this.seekBar_.addEventListener(
'mouseup', this.onSeekEnd_.bind(this));
this.muteButton_.addEventListener(
'click', this.onMuteClick_.bind(this));
this.volumeBar_.addEventListener(
sliderInputEvent, this.onVolumeInput_.bind(this));
this.video_.addEventListener(
'volumechange', this.onVolumeStateChange_.bind(this));
// initialize volume display with a fake event
this.onVolumeStateChange_();
this.captionButton_.addEventListener(
'click', this.onCaptionClick_.bind(this));
this.player_.addEventListener(
'texttrackvisibility', this.onCaptionStateChange_.bind(this));
this.player_.addEventListener(
'trackschanged', this.onTracksChange_.bind(this));
// initialize caption state with a fake event
this.onCaptionStateChange_();
this.fullscreenButton_.addEventListener(
'click', this.onFullscreenClick_.bind(this));
this.currentTime_.addEventListener(
'click', this.onCurrentTimeClick_.bind(this));
this.rewindButton_.addEventListener(
'click', this.onRewindClick_.bind(this));
this.fastForwardButton_.addEventListener(
'click', this.onFastForwardClick_.bind(this));
this.castButton_.addEventListener(
'click', this.onCastClick_.bind(this));
this.videoContainer_.addEventListener(
'touchstart', this.onContainerTouch_.bind(this));
this.videoContainer_.addEventListener(
'click', this.onPlayPauseClick_.bind(this));
// Clicks in the controls should not propagate up to the video container.
this.controls_.addEventListener(
'click', function(event) { event.stopPropagation(); });
this.videoContainer_.addEventListener(
'mousemove', this.onMouseMove_.bind(this));
this.videoContainer_.addEventListener(
'touchmove', this.onMouseMove_.bind(this));
this.videoContainer_.addEventListener(
'touchend', this.onMouseMove_.bind(this));
this.videoContainer_.addEventListener(
'mouseout', this.onMouseOut_.bind(this));
this.castProxy_.addEventListener(
'caststatuschanged', this.onCastStatusChange_.bind(this));
};
/**
* Initializes minimal player controls. Used on both sender (indirectly) and
* receiver (directly).
* @param {HTMLMediaElement} video
* @param {shaka.Player} player
*/
ShakaControls.prototype.initMinimal = function(video, player) {
this.video_ = video;
this.player_ = player;
this.player_.addEventListener(
'buffering', this.onBufferingStateChange_.bind(this));
window.setInterval(this.updateTimeAndSeekRange_.bind(this), 125);
};
/**
* This allows the application to inhibit casting.
*
* @param {boolean} allow
*/
ShakaControls.prototype.allowCast = function(allow) {
this.castAllowed_ = allow;
this.onCastStatusChange_(null);
};
/**
* Used by the application to notify the controls that a load operation is
* complete. This allows the controls to recalculate play/paused state, which
* is important for platforms like Android where autoplay is disabled.
*/
ShakaControls.prototype.loadComplete = function() {
// If we are on Android or if autoplay is false, video.paused should be true.
// Otherwise, video.paused is false and the content is autoplaying.
this.onPlayStateChange_();
};
/**
* Hiding the cursor when the mouse stops moving seems to be the only decent UX
* in fullscreen mode. Since we can't use pure CSS for that, we use events both
* in and out of fullscreen mode.
* @param {!Event} event
* @private
*/
ShakaControls.prototype.onMouseMove_ = function(event) {
if (event.type == 'touchstart' || event.type == 'touchmove' ||
event.type == 'touchend') {
this.lastTouchEventTime_ = Date.now();
} else if (this.lastTouchEventTime_ + 1000 < Date.now()) {
// It has been a while since the last touch event, this is probably a real
// mouse moving, so treat it like a mouse.
this.lastTouchEventTime_ = null;
}
// Use the cursor specified in the CSS file.
this.videoContainer_.style.cursor = '';
// Show the controls.
this.controls_.style.opacity = 1;
this.updateTimeAndSeekRange_();
// Hide the cursor when the mouse stops moving.
// Only applies while the cursor is over the video container.
if (this.mouseStillTimeoutId_) {
// Reset the timer.
window.clearTimeout(this.mouseStillTimeoutId_);
}
// Only start a timeout on 'touchend' or for 'mousemove' with no touch events.
if (event.type == 'touchend' || !this.lastTouchEventTime_) {
this.mouseStillTimeoutId_ = window.setTimeout(
this.onMouseStill_.bind(this), 3000);
}
};
/** @private */
ShakaControls.prototype.onMouseOut_ = function() {
// Expire the timer early.
if (this.mouseStillTimeoutId_) {
window.clearTimeout(this.mouseStillTimeoutId_);
}
// Run the timeout callback to hide the controls.
// If we don't, the opacity style we set in onMouseMove_ will continue to
// override the opacity in CSS and force the controls to stay visible.
this.onMouseStill_();
};
/** @private */
ShakaControls.prototype.onMouseStill_ = function() {
// The mouse has stopped moving.
this.mouseStillTimeoutId_ = null;
// Hide the cursor. (NOTE: not supported on IE)
this.videoContainer_.style.cursor = 'none';
// Revert opacity control to CSS. Hovering directly over the controls will
// keep them showing, even in fullscreen mode. Unless there were touch events,
// then override the hover and hide the controls.
this.controls_.style.opacity = this.lastTouchEventTime_ ? '0' : '';
};
/**
* @param {!Event} event
* @private
*/
ShakaControls.prototype.onContainerTouch_ = function(event) {
if (!this.video_.duration) {
// Can't play yet. Ignore.
return;
}
if (this.controls_.style.opacity == 1) {
this.lastTouchEventTime_ = Date.now();
// The controls are showing.
// Let this event continue and become a click.
} else {
// The controls are hidden, so show them.
this.onMouseMove_(event);
// Stop this event from becoming a click event.
event.preventDefault();
}
};
/** @private */
ShakaControls.prototype.onPlayPauseClick_ = function() {
if (!this.video_.duration) {
// Can't play yet. Ignore.
return;
}
this.player_.cancelTrickPlay();
this.trickPlayRate_ = 1;
if (this.video_.paused) {
this.video_.play();
} else {
this.video_.pause();
}
};
/** @private */
ShakaControls.prototype.onPlayStateChange_ = function() {
// Video is paused during seek, so don't show the play arrow while seeking:
if (this.video_.paused && !this.isSeeking_) {
this.playPauseButton_.textContent = 'play_arrow';
this.giantPlayButtonContainer_.style.display = 'inline';
} else {
this.playPauseButton_.textContent = 'pause';
this.giantPlayButtonContainer_.style.display = 'none';
}
};
/** @private */
ShakaControls.prototype.onSeekStart_ = function() {
this.isSeeking_ = true;
this.video_.pause();
};
/** @private */
ShakaControls.prototype.onSeekInput_ = function() {
if (!this.video_.duration) {
// Can't seek yet. Ignore.
return;
}
// Update the UI right away.
this.updateTimeAndSeekRange_();
// Collect input events and seek when things have been stable for 125ms.
if (this.seekTimeoutId_ != null) {
window.clearTimeout(this.seekTimeoutId_);
}
this.seekTimeoutId_ = window.setTimeout(
this.onSeekInputTimeout_.bind(this), 125);
};
/** @private */
ShakaControls.prototype.onSeekInputTimeout_ = function() {
this.seekTimeoutId_ = null;
this.video_.currentTime = parseFloat(this.seekBar_.value);
};
/** @private */
ShakaControls.prototype.onSeekEnd_ = function() {
if (this.seekTimeoutId_ != null) {
// They just let go of the seek bar, so end the timer early.
window.clearTimeout(this.seekTimeoutId_);
this.onSeekInputTimeout_();
}
this.isSeeking_ = false;
this.video_.play();
};
/** @private */
ShakaControls.prototype.onMuteClick_ = function() {
this.video_.muted = !this.video_.muted;
};
/**
* Updates the controls to reflect volume changes.
* @private
*/
ShakaControls.prototype.onVolumeStateChange_ = function() {
if (this.video_.muted) {
this.muteButton_.textContent = 'volume_off';
this.volumeBar_.value = 0;
} else {
this.muteButton_.textContent = 'volume_up';
this.volumeBar_.value = this.video_.volume;
}
var gradient = ['to right'];
gradient.push('#ccc ' + (this.volumeBar_.value * 100) + '%');
gradient.push('#000 ' + (this.volumeBar_.value * 100) + '%');
gradient.push('#000 100%');
this.volumeBar_.style.background =
'linear-gradient(' + gradient.join(',') + ')';
};
/** @private */
ShakaControls.prototype.onVolumeInput_ = function() {
this.video_.volume = parseFloat(this.volumeBar_.value);
this.video_.muted = false;
};
/** @private */
ShakaControls.prototype.onCaptionClick_ = function() {
this.player_.setTextTrackVisibility(!this.player_.isTextTrackVisible());
};
/** @private */
ShakaControls.prototype.onTracksChange_ = function() {
var hasText = this.player_.getTracks().some(function(track) {
return track.type == 'text';
});
this.captionButton_.style.display = hasText ? 'inherit' : 'none';
};
/** @private */
ShakaControls.prototype.onCaptionStateChange_ = function() {
if (this.player_.isTextTrackVisible()) {
this.captionButton_.style.color = 'white';
} else {
// Make the button look darker to show that the text track is inactive.
this.captionButton_.style.color = 'rgba(255, 255, 255, 0.3)';
}
};
/** @private */
ShakaControls.prototype.onFullscreenClick_ = function() {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
this.videoContainer_.requestFullscreen();
}
};
/** @private */
ShakaControls.prototype.onCurrentTimeClick_ = function() {
// Jump to LIVE if the user clicks on the current time.
if (this.player_.isLive()) {
this.video_.currentTime = this.seekBar_.max;
}
};
/**
* Cycles trick play rate between -1, -2, -4, and -8.
* @private
*/
ShakaControls.prototype.onRewindClick_ = function() {
if (!this.video_.duration) {
return;
}
this.trickPlayRate_ = (this.trickPlayRate_ > 0 || this.trickPlayRate_ < -4) ?
-1 : this.trickPlayRate_ * 2;
this.player_.trickPlay(this.trickPlayRate_);
};
/**
* Cycles trick play rate between 1, 2, 4, and 8.
* @private
*/
ShakaControls.prototype.onFastForwardClick_ = function() {
if (!this.video_.duration) {
return;
}
this.trickPlayRate_ = (this.trickPlayRate_ < 0 || this.trickPlayRate_ > 4) ?
1 : this.trickPlayRate_ * 2;
this.player_.trickPlay(this.trickPlayRate_);
};
/** @private */
ShakaControls.prototype.onCastClick_ = function() {
if (this.castProxy_.isCasting()) {
this.castProxy_.suggestDisconnect();
} else {
this.castButton_.disabled = true;
this.castProxy_.cast().then(function() {
this.castButton_.disabled = false;
// Success!
}.bind(this), function(error) {
this.castButton_.disabled = false;
if (error.code != shaka.util.Error.Code.CAST_CANCELED_BY_USER) {
this.onError_(error);
}
}.bind(this));
}
};
/**
* @param {Event} event
* @private
*/
ShakaControls.prototype.onCastStatusChange_ = function(event) {
var canCast = this.castProxy_.canCast() && this.castAllowed_;
var isCasting = this.castProxy_.isCasting();
this.notifyCastStatus_(isCasting);
this.castButton_.style.display = canCast ? 'inherit' : 'none';
this.castButton_.textContent = isCasting ? 'cast_connected' : 'cast';
this.castReceiverName_.style.display =
isCasting ? 'inherit' : 'none';
this.castReceiverName_.textContent =
isCasting ? 'Casting to ' + this.castProxy_.receiverName() : '';
this.controls_.classList.toggle('casting', this.castProxy_.isCasting());
};
/**
* @param {Event} event
* @private
*/
ShakaControls.prototype.onBufferingStateChange_ = function(event) {
this.bufferingSpinner_.style.display =
event.buffering ? 'inherit' : 'none';
};
/**
* @param {boolean} show True to show trick play controls, false to show seek
* bar.
*/
ShakaControls.prototype.showTrickPlay = function(show) {
this.seekBar_.parentElement.style.width = show ? 'auto' : '100%';
this.seekBar_.style.display = show ? 'none' : 'flex';
this.rewindButton_.style.display = show ? 'inline' : 'none';
this.fastForwardButton_.style.display = show ? 'inline' : 'none';
};
/**
* @return {boolean}
* @private
*/
ShakaControls.prototype.isOpaque_ = function() {
var parentElement = this.controls_.parentElement;
// The controls are opaque if either:
// 1. We have explicitly made them so in JavaScript
// 2. The browser has made them so via css and the hover state
return (this.controls_.style.opacity == 1 ||
parentElement.querySelector('#controls:hover') == this.controls_);
};
/**
* Called when the seek range or current time need to be updated.
* @private
*/
ShakaControls.prototype.updateTimeAndSeekRange_ = function() {
// Suppress updates if the controls are hidden.
if (!this.isOpaque_()) {
return;
}
var displayTime = this.isSeeking_ ?
this.seekBar_.value : this.video_.currentTime;
var duration = this.video_.duration;
var bufferedLength = this.video_.buffered.length;
var bufferedStart = bufferedLength ? this.video_.buffered.start(0) : 0;
var bufferedEnd = bufferedLength ? this.video_.buffered.end(0) : 0;
var seekRange = this.player_.seekRange();
this.seekBar_.min = seekRange.start;
this.seekBar_.max = seekRange.end;
if (this.player_.isLive()) {
// The amount of time we are behind the live edge.
var behindLive = Math.floor(seekRange.end - displayTime);
displayTime = Math.max(0, behindLive);
var showHour = (seekRange.end - seekRange.start) >= 3600;
// Consider "LIVE" when less than 1 second behind the live-edge. Always
// show the full time string when seeking, including the leading '-';
// otherwise, the time string "flickers" near the live-edge.
if ((displayTime >= 1) || this.isSeeking_) {
this.currentTime_.textContent =
'- ' + this.buildTimeString_(displayTime, showHour);
this.currentTime_.style.cursor = 'pointer';
} else {
this.currentTime_.textContent = 'LIVE';
this.currentTime_.style.cursor = '';
}
if (!this.isSeeking_) {
this.seekBar_.value = seekRange.end - displayTime;
}
} else {
var showHour = duration >= 3600;
this.currentTime_.textContent =
this.buildTimeString_(displayTime, showHour);
if (!this.isSeeking_) {
this.seekBar_.value = displayTime;
}
this.currentTime_.style.cursor = '';
}
var gradient = ['to right'];
if (bufferedLength == 0) {
gradient.push('#000 0%');
} else {
// NOTE: the fallback to zero eliminates NaN.
var bufferStartFraction = (bufferedStart / duration) || 0;
var bufferEndFraction = (bufferedEnd / duration) || 0;
var playheadFraction = (displayTime / duration) || 0;
if (this.player_.isLive()) {
var bufferStart = Math.max(bufferedStart, seekRange.start);
var bufferEnd = Math.min(bufferedEnd, seekRange.end);
var seekRangeSize = seekRange.end - seekRange.start;
var bufferStartDistance = bufferStart - seekRange.start;
var bufferEndDistance = bufferEnd - seekRange.start;
var playheadDistance = displayTime - seekRange.start;
bufferStartFraction = (bufferStartDistance / seekRangeSize) || 0;
bufferEndFraction = (bufferEndDistance / seekRangeSize) || 0;
playheadFraction = (playheadDistance / seekRangeSize) || 0;
}
gradient.push('#000 ' + (bufferStartFraction * 100) + '%');
gradient.push('#ccc ' + (bufferStartFraction * 100) + '%');
gradient.push('#ccc ' + (playheadFraction * 100) + '%');
gradient.push('#444 ' + (playheadFraction * 100) + '%');
gradient.push('#444 ' + (bufferEndFraction * 100) + '%');
gradient.push('#000 ' + (bufferEndFraction * 100) + '%');
}
this.seekBar_.style.background =
'linear-gradient(' + gradient.join(',') + ')';
};
/**
* Builds a time string, e.g., 01:04:23, from |displayTime|.
*
* @param {number} displayTime
* @param {boolean} showHour
* @return {string}
* @private
*/
ShakaControls.prototype.buildTimeString_ = function(displayTime, showHour) {
var h = Math.floor(displayTime / 3600);
var m = Math.floor((displayTime / 60) % 60);
var s = Math.floor(displayTime % 60);
if (s < 10) s = '0' + s;
var text = m + ':' + s;
if (showHour) {
if (m < 10) text = '0' + text;
text = h + ':' + text;
}
return text;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | 1 1 1 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @namespace */
var ShakaDemoUtils = {};
/**
* @param {shakaAssets.AssetInfo} asset
* @param {shaka.Player} player
*/
ShakaDemoUtils.setupAssetMetadata = function(asset, player) {
var config = /** @type {shakaExtern.PlayerConfiguration} */(
{ drm: {}, manifest: { dash: {} } });
// Add config from this asset.
if (asset.licenseServers)
config.drm.servers = asset.licenseServers;
if (asset.drmCallback)
config.manifest.dash.customScheme = asset.drmCallback;
if (asset.clearKeys)
config.drm.clearKeys = asset.clearKeys;
player.configure(config);
// Configure network filters.
var networkingEngine = player.getNetworkingEngine();
networkingEngine.clearAllRequestFilters();
networkingEngine.clearAllResponseFilters();
if (asset.licenseRequestHeaders) {
var filter = ShakaDemoUtils.addLicenseRequestHeaders_.bind(
null, asset.licenseRequestHeaders);
networkingEngine.registerRequestFilter(filter);
}
if (asset.requestFilter)
networkingEngine.registerRequestFilter(asset.requestFilter);
if (asset.responseFilter)
networkingEngine.registerResponseFilter(asset.responseFilter);
if (asset.extraConfig)
player.configure(/** @type {shakaExtern.PlayerConfiguration} */(
asset.extraConfig));
};
/**
* @param {!Object.<string, string>} headers
* @param {shaka.net.NetworkingEngine.RequestType} requestType
* @param {shakaExtern.Request} request
* @private
*/
ShakaDemoUtils.addLicenseRequestHeaders_ =
function(headers, requestType, request) {
if (requestType != shaka.net.NetworkingEngine.RequestType.LICENSE) return;
// Add these to the existing headers. Do not clobber them!
// For PlayReady, there will already be headers in the request.
for (var k in headers) {
request.headers[k] = headers[k];
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | 1 1 1 1 1 1 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Shaka Player demo, main section.
*
* @suppress {visibility} to work around compiler errors until we can
* refactor the demo into classes that talk via public method. TODO
*/
/** @suppress {duplicate} */
var shakaDemo = shakaDemo || {};
/** @private */
shakaDemo.setupInfo_ = function() {
window.setInterval(shakaDemo.updateDebugInfo_, 125);
shakaDemo.player_.addEventListener(
'trackschanged', shakaDemo.onTracksChanged_);
shakaDemo.player_.addEventListener(
'adaptation', shakaDemo.onAdaptation_);
document.getElementById('videoTracks').addEventListener(
'change', shakaDemo.onTrackSelected_);
document.getElementById('audioTracks').addEventListener(
'change', shakaDemo.onTrackSelected_);
document.getElementById('textTracks').addEventListener(
'change', shakaDemo.onTrackSelected_);
};
/**
* @param {!Event} event
* @private
*/
shakaDemo.onTracksChanged_ = function(event) {
// Update the track lists.
var lists = {
video: document.getElementById('videoTracks'),
audio: document.getElementById('audioTracks'),
text: document.getElementById('textTracks')
};
var formatters = {
video: function(track) {
return track.width + 'x' + track.height + ', ' +
track.bandwidth + ' bits/s';
},
audio: function(track) {
return 'language: ' + track.language + ', ' +
track.bandwidth + ' bits/s';
},
text: function(track) {
return 'language: ' + track.language + ' ' +
'(' + track.kind + ')';
}
};
// Clear the old track lists.
Object.keys(lists).forEach(function(type) {
var list = lists[type];
while (list.firstChild) {
list.removeChild(list.firstChild);
}
});
// Populate with the new tracks.
var tracks = shakaDemo.player_.getTracks();
tracks.sort(function(t1, t2) {
// Sort by language, then by bandwidth.
if (t1.language) {
var ret = t1.language.localeCompare(t2.language);
if (ret) return ret;
}
return t1.bandwidth - t2.bandwidth;
});
tracks.forEach(function(track) {
var list = lists[track.type];
if (!list) return;
var option = document.createElement('option');
option.textContent = formatters[track.type](track);
option.track = track;
option.value = track.id;
option.selected = track.active;
list.appendChild(option);
});
};
/**
* @param {!Event} event
* @private
*/
shakaDemo.onAdaptation_ = function(event) {
var lists = {
video: document.getElementById('videoTracks'),
audio: document.getElementById('audioTracks'),
text: document.getElementById('textTracks')
};
// Find the rows for the active tracks and select them.
var tracks = shakaDemo.player_.getTracks();
tracks.forEach(function(track) {
if (!track.active) return;
var list = lists[track.type];
for (var i = 0; i < list.options.length; ++i) {
var option = list.options[i];
if (option.value == track.id) {
option.selected = true;
break;
}
}
});
};
/**
* @param {!Event} event
* @private
*/
shakaDemo.onTrackSelected_ = function(event) {
var list = event.target;
var option = list.options[list.selectedIndex];
var track = option.track;
var player = shakaDemo.player_;
player.selectTrack(track, /* clearBuffer */ true);
// Adaptation might have been changed by calling selectTrack().
// Update the adaptation checkbox.
var enableAdaptation = player.getConfiguration().abr.enabled;
document.getElementById('enableAdaptation').checked = enableAdaptation;
};
/** @private */
shakaDemo.updateDebugInfo_ = function() {
var video = shakaDemo.video_;
document.getElementById('videoResDebug').textContent =
video.videoWidth + ' x ' + video.videoHeight;
var behind = 0;
var ahead = 0;
var currentTime = video.currentTime;
var buffered = video.buffered;
for (var i = 0; i < buffered.length; ++i) {
if (buffered.start(i) <= currentTime && buffered.end(i) >= currentTime) {
ahead = buffered.end(i) - currentTime;
behind = currentTime - buffered.start(i);
break;
}
}
document.getElementById('bufferedDebug').textContent =
'- ' + behind.toFixed(0) + 's / ' + '+ ' + ahead.toFixed(0) + 's';
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | 2 2 1 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Loads the library. Chooses compiled or debug version of the library based
* on the presence or absence of the URL parameter "compiled".
*
* This dynamic loading process is not necessary in a production environment,
* but greatly simplifies the process of switching between compiled and
* uncompiled mode during development.
*
* This is used in the provided demo app, but can also be used to load the
* uncompiled version of the library into your own application environment.
*/
(function() { // anonymous namespace
// The sources may be in a different folder from the app.
// Compute the base URL for all library sources.
var currentScript = document.currentScript ||
document.scripts[document.scripts.length - 1];
var loaderSrc = currentScript.src;
var baseUrl = loaderSrc.split('/').slice(0, -1).join('/') + '/';
function loadScript(src) {
// This does not seem like it would be the best way to do this, but the
// timing is different than creating a new script element and appending
// it to the head element. This way, all script loading happens before
// DOMContentLoaded. This is also compatible with goog.require's loading
// mechanism, whereas appending an element to head isn't.
document.write('<script src="' + baseUrl + src + '"></script>');
}
var fields = location.search.split('?').slice(1).join('?');
fields = fields ? fields.split(';') : [];
// Very old browsers do not have Array.prototype.indexOf.
var compiledMode = false;
for (var i = 0; i < fields.length; ++i) {
if (fields[i] == 'compiled') {
compiledMode = true;
break;
}
}
if (compiledMode) {
// This contains the entire library, compiled in debug mode.
loadScript('../dist/shaka-player.compiled.debug.js');
} else {
// In non-compiled mode, we load the closure library and the generated deps
// file to bootstrap the system. goog.require will load the rest.
loadScript('../third_party/closure/goog/base.js');
loadScript('../dist/deps.js');
// This file contains goog.require calls for all exported classes.
loadScript('../shaka-player.uncompiled.js');
}
})(); // anonymous namespace
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | 2 2 2 2 2 2 2 2 2 2 2 2 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Shaka Player demo, main section.
*
* @suppress {visibility} to work around compiler errors until we can
* refactor the demo into classes that talk via public method. TODO
*/
/** @suppress {duplicate} */
var shakaDemo = shakaDemo || {};
/** @private {shaka.cast.CastProxy} */
shakaDemo.castProxy_ = null;
/** @private {HTMLMediaElement} */
shakaDemo.video_ = null;
/** @private {shaka.Player} */
shakaDemo.player_ = null;
/** @private {shaka.Player} */
shakaDemo.localPlayer_ = null;
/** @private {shakaExtern.SupportType} */
shakaDemo.support_;
/** @private {ShakaControls} */
shakaDemo.controls_ = null;
/**
* The registered ID of the v2 Chromecast receiver demo.
* @const {string}
* @private
*/
shakaDemo.CC_APP_ID_ = '4E839F3A';
/**
* Initialize the application.
*/
shakaDemo.init = function() {
document.getElementById('errorDisplayCloseButton').addEventListener(
'click', shakaDemo.closeError);
// Display the version number.
document.getElementById('version').textContent = shaka.Player.version;
// Fill in the language preferences based on browser config, if available.
var language = navigator.language || 'en-us';
document.getElementById('preferredAudioLanguage').value = language;
document.getElementById('preferredTextLanguage').value = language;
// Read URL parameters.
var fields = location.search.split('?').slice(1).join('?');
fields = fields ? fields.split(';') : [];
var params = {};
for (var i = 0; i < fields.length; ++i) {
var kv = fields[i].split('=');
params[kv[0]] = kv.slice(1).join('=');
}
if ('lang' in params) {
document.getElementById('preferredAudioLanguage').value = params['lang'];
document.getElementById('preferredTextLanguage').value = params['lang'];
}
if ('asset' in params) {
document.getElementById('manifestInput').value = params['asset'];
}
if ('license' in params) {
document.getElementById('licenseServerInput').value = params['license'];
}
if ('logtoscreen' in params) {
document.getElementById('logToScreen').checked = true;
}
if ('noinput' in params) {
// Both the content container and body need different styles in this mode.
document.getElementById('container').className = 'noinput';
document.body.className = 'noinput';
}
if ('vv' in params && shaka.log) {
shaka.log.setLevel(shaka.log.Level.V2);
} else if ('v' in params && shaka.log) {
shaka.log.setLevel(shaka.log.Level.V1);
} else if ('debug' in params && shaka.log) {
shaka.log.setLevel(shaka.log.Level.DEBUG);
}
shakaDemo.setupLogging_();
shaka.polyfill.installAll();
if (!shaka.Player.isBrowserSupported()) {
var errorDisplayLink = document.getElementById('errorDisplayLink');
var error = 'Your browser is not supported!';
// IE8 and other very old browsers don't have textContent.
if (errorDisplayLink.textContent === undefined) {
errorDisplayLink.innerText = error;
} else {
errorDisplayLink.textContent = error;
}
// Disable the load button.
var loadButton = document.getElementById('loadButton');
loadButton.disabled = true;
// Hide the error message's close button.
var errorDisplayCloseButton =
document.getElementById('errorDisplayCloseButton');
errorDisplayCloseButton.style.display = 'none';
// Make sure the error is seen.
errorDisplayLink.style.fontSize = '250%';
// TODO: Link to docs about browser support. For now, disable link.
errorDisplayLink.href = '#';
// Disable for newer browsers:
errorDisplayLink.style.pointerEvents = 'none';
// Disable for older browsers:
errorDisplayLink.style.textDecoration = 'none';
errorDisplayLink.style.cursor = 'default';
errorDisplayLink.onclick = function() { return false; };
var errorDisplay = document.getElementById('errorDisplay');
errorDisplay.style.display = 'block';
} else {
shaka.Player.probeSupport().then(function(support) {
shakaDemo.support_ = support;
var localVideo =
/** @type {!HTMLVideoElement} */(document.getElementById('video'));
var localPlayer = new shaka.Player(localVideo);
shakaDemo.castProxy_ = new shaka.cast.CastProxy(
localVideo, localPlayer, shakaDemo.CC_APP_ID_);
shakaDemo.video_ = shakaDemo.castProxy_.getVideo();
shakaDemo.player_ = shakaDemo.castProxy_.getPlayer();
shakaDemo.player_.addEventListener('error', shakaDemo.onErrorEvent_);
shakaDemo.localPlayer_ = localPlayer;
shakaDemo.setupAssets_();
shakaDemo.setupOffline_();
shakaDemo.setupConfiguration_();
shakaDemo.setupInfo_();
shakaDemo.controls_ = new ShakaControls();
shakaDemo.controls_.init(shakaDemo.castProxy_, shakaDemo.onError_,
shakaDemo.onCastStatusChange_);
// If a custom asset was given in the URL, select it now.
if ('asset' in params) {
var assetList = document.getElementById('assetList');
var customAsset = document.getElementById('customAsset');
assetList.selectedIndex = assetList.options.length - 1;
customAsset.style.display = 'block';
}
if ('play' in params) {
shakaDemo.load();
}
});
}
};
/**
* @param {!Event} event
* @private
*/
shakaDemo.onErrorEvent_ = function(event) {
var error = event.detail;
shakaDemo.onError_(error);
};
/**
* @param {!shaka.util.Error} error
* @private
*/
shakaDemo.onError_ = function(error) {
console.error('Player error', error);
var message = error.message || ('Error code ' + error.code);
var link = document.getElementById('errorDisplayLink');
link.href = '../docs/api/shaka.util.Error.html#value:' + error.code;
link.textContent = message;
// Make the link clickable only if we have an error code.
link.style.pointerEvents = error.code ? 'auto' : 'none';
document.getElementById('errorDisplay').style.display = 'block';
};
/**
* Closes the error bar.
*/
shakaDemo.closeError = function() {
document.getElementById('errorDisplay').style.display = 'none';
var link = document.getElementById('errorDisplayLink');
link.href = '';
link.textContent = '';
};
// IE 9 fires DOMContentLoaded, and enters the "interactive"
// readyState, before document.body has been initialized, so wait
// for window.load.
if (document.readyState == 'loading' ||
document.readyState == 'interactive') {
if (window.attachEvent) {
// IE8
window.attachEvent('onload', shakaDemo.init);
} else {
window.addEventListener('load', shakaDemo.init);
}
} else {
shakaDemo.init();
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | 1 1 1 1 1 1 1 1 1 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Shaka Player demo, main section.
*
* @suppress {visibility} to work around compiler errors until we can
* refactor the demo into classes that talk via public method. TODO
*/
/** @suppress {duplicate} */
var shakaDemo = shakaDemo || {};
/** @private {?HTMLOptGroupElement} */
shakaDemo.offlineOptGroup_ = null;
/** @private {boolean} */
shakaDemo.offlineOperationInProgress_ = false;
/**
* @param {boolean} canHide True to hide the progress value if there isn't an
* operation going.
* @private
*/
shakaDemo.updateButtons_ = function(canHide) {
var assetList = document.getElementById('assetList');
var inProgress = shakaDemo.offlineOperationInProgress_;
document.getElementById('progressDiv').style.display =
canHide && !inProgress ? 'none' : 'block';
var option = assetList.options[assetList.selectedIndex];
var storedContent = option.storedContent;
// True if there is no DRM or if the browser supports persistent licenses for
// any given DRM system.
var supportsDrm = !option.asset || !option.asset.drm ||
!option.asset.drm.length || option.asset.drm.some(function(drm) {
return shakaDemo.support_.drm[drm] &&
shakaDemo.support_.drm[drm].persistentState;
});
// Only show when the custom asset option is selected.
document.getElementById('offlineNameDiv').style.display =
option.asset ? 'none' : 'block';
var button = document.getElementById('storeDelete');
button.disabled = (inProgress || !supportsDrm || option.isStored);
button.innerText = storedContent ? 'Delete' : 'Store';
if (inProgress)
button.title = 'There is already an operation in progress';
else if (!supportsDrm)
button.title = 'This browser does not support persistent licenses';
else if (button.disabled)
button.title = 'Selected asset is already stored offline';
else
button.title = '';
};
/** @private */
shakaDemo.setupOffline_ = function() {
document.getElementById('storeDelete')
.addEventListener('click', shakaDemo.storeDeleteAsset_);
document.getElementById('assetList')
.addEventListener('change', shakaDemo.updateButtons_.bind(null, true));
shakaDemo.updateButtons_(true);
};
/**
* @return {!Promise}
* @private
*/
shakaDemo.setupOfflineAssets_ = function() {
var Storage = shaka.offline.Storage;
if (!Storage.support()) {
var section = document.getElementById('offlineSection');
section.style.display = 'none';
return Promise.resolve();
}
/** @type {!HTMLOptGroupElement} */
var group;
var assetList = document.getElementById('assetList');
if (!shakaDemo.offlineOptGroup_) {
group =
/** @type {!HTMLOptGroupElement} */ (
document.createElement('optgroup'));
shakaDemo.offlineOptGroup_ = group;
group.label = 'Offline';
assetList.appendChild(group);
} else {
group = shakaDemo.offlineOptGroup_;
}
var db = new Storage(shakaDemo.localPlayer_);
return db.list().then(function(storedContents) {
storedContents.forEach(function(storedContent) {
for (var i = 0; i < assetList.options.length; i++) {
var option = assetList.options[i];
if (option.asset &&
option.asset.manifestUri == storedContent.originalManifestUri) {
option.isStored = true;
break;
}
}
var asset = {manifestUri: storedContent.offlineUri};
var option = document.createElement('option');
option.textContent =
storedContent.appMetadata ? storedContent.appMetadata.name : '';
option.asset = asset;
option.storedContent = storedContent;
group.appendChild(option);
});
shakaDemo.updateButtons_(true);
return db.destroy();
});
};
/** @private */
shakaDemo.storeDeleteAsset_ = function() {
shakaDemo.closeError();
shakaDemo.offlineOperationInProgress_ = true;
shakaDemo.updateButtons_(false);
var assetList = document.getElementById('assetList');
var progress = document.getElementById('progress');
var option = assetList.options[assetList.selectedIndex];
progress.textContent = '0';
var storage = new shaka.offline.Storage(shakaDemo.player_);
storage.configure(/** @type {shakaExtern.OfflineConfiguration} */ ({
progressCallback: function(data, percent) {
progress.textContent = (percent * 100).toFixed(2);
}
}));
var p;
if (option.storedContent) {
var originalManifestUri = option.storedContent.originalManifestUri;
p = storage.remove(option.storedContent).then(function() {
for (var i = 0; i < assetList.options.length; i++) {
var option = assetList.options[i];
if (option.asset && option.asset.manifestUri == originalManifestUri)
option.isStored = false;
}
shakaDemo.refreshAssetList_();
});
} else {
var asset = shakaDemo.preparePlayer_(option.asset);
var nameField = document.getElementById('offlineName').value;
var assetName = asset.name ? asset.name + ' (offline)' : null;
var metadata = {name: assetName || nameField || asset.manifestUri};
p = storage.store(asset.manifestUri, metadata).then(function() {
shakaDemo.refreshAssetList_();
if (option.asset)
option.isStored = true;
});
}
p.catch(function(reason) {
var error = /** @type {!shaka.util.Error} */(reason);
shakaDemo.onError_(error);
}).then(function() {
shakaDemo.offlineOperationInProgress_ = false;
shakaDemo.updateButtons_(false);
return storage.destroy();
});
};
/** @private */
shakaDemo.refreshAssetList_ = function() {
// Remove all child elements.
var group = shakaDemo.offlineOptGroup_;
while (group.firstChild) {
group.removeChild(group.firstChild);
}
shakaDemo.setupOfflineAssets_();
};
/**
* @param {boolean} connected
* @private
*/
shakaDemo.onCastStatusChange_ = function(connected) {
if (!shakaDemo.offlineOptGroup_) {
// No offline support.
return;
}
// When we are casting, offline assets become unavailable.
shakaDemo.offlineOptGroup_.disabled = connected;
if (connected) {
var assetList = document.getElementById('assetList');
var option = assetList.options[assetList.selectedIndex];
if (option.storedContent) {
// This is an offline asset. Select something else.
assetList.selectedIndex = 0;
}
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | 1 2 2 2 2 2 2 1 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* A Chromecast receiver demo app.
* @constructor
* @suppress {missingProvide}
*/
function ShakaReceiver() {
/** @private {HTMLMediaElement} */
this.video_ = null;
/** @private {shaka.Player} */
this.player_ = null;
/** @private {shaka.cast.CastReceiver} */
this.receiver_ = null;
/** @private {Element} */
this.pauseIcon_ = null;
/** @private {Element} */
this.controlsElement_ = null;
/** @private {ShakaControls} */
this.controlsUi_ = null;
/** @private {?number} */
this.controlsTimerId_ = null;
/** @private {Element} */
this.idle_ = null;
/** @private {?number} */
this.idleTimerId_ = null;
/**
* In seconds.
* @const
* @private {number}
*/
this.idleTimeout_ = 300;
}
/**
* Initialize the application.
*/
ShakaReceiver.prototype.init = function() {
shaka.polyfill.installAll();
this.video_ =
/** @type {!HTMLMediaElement} */(document.getElementById('video'));
this.player_ = new shaka.Player(this.video_);
this.controlsUi_ = new ShakaControls();
this.controlsUi_.initMinimal(this.video_, this.player_);
this.controlsElement_ = document.getElementById('controls');
this.pauseIcon_ = document.getElementById('pauseIcon');
this.idle_ = document.getElementById('idle');
this.video_.addEventListener(
'play', this.onPlayStateChange_.bind(this));
this.video_.addEventListener(
'pause', this.onPlayStateChange_.bind(this));
this.video_.addEventListener(
'seeking', this.onPlayStateChange_.bind(this));
this.video_.addEventListener(
'emptied', this.onPlayStateChange_.bind(this));
this.receiver_ = new shaka.cast.CastReceiver(
this.video_, this.player_, this.appDataCallback_.bind(this));
this.receiver_.addEventListener(
'caststatuschanged', this.checkIdle_.bind(this));
this.startIdleTimer_();
};
/**
* @param {Object} appData
* @private
*/
ShakaReceiver.prototype.appDataCallback_ = function(appData) {
// appData is null if we start the app without any media loaded.
if (!appData) return;
var asset = /** @type {shakaAssets.AssetInfo} */(appData['asset']);
// Patch in non-transferable callbacks for YT DRM:
if (appData['isYtDrm']) {
asset.drmCallback = shakaAssets.YouTubeCallback;
asset.requestFilter = shakaAssets.YouTubeRequestFilter;
asset.responseFilter = shakaAssets.YouTubeResponseFilter;
}
ShakaDemoUtils.setupAssetMetadata(asset, this.player_);
};
/** @private */
ShakaReceiver.prototype.checkIdle_ = function() {
console.debug('status changed',
'idle=', this.receiver_.isIdle());
// If the app is idle, show the idle card and set a timer to close the app.
// Otherwise, hide the idle card and cancel the timer.
if (this.receiver_.isIdle()) {
this.idle_.style.display = 'block';
this.startIdleTimer_();
} else {
this.idle_.style.display = 'none';
this.cancelIdleTimer_();
}
};
/** @private */
ShakaReceiver.prototype.startIdleTimer_ = function() {
this.cancelIdleTimer_();
this.idleTimerId_ = window.setTimeout(
window.close.bind(window), this.idleTimeout_ * 1000.0);
};
/** @private */
ShakaReceiver.prototype.cancelIdleTimer_ = function() {
if (this.idleTimerId_ != null) {
window.clearTimeout(this.idleTimerId_);
this.idleTimerId_ = null;
}
};
/** @private */
ShakaReceiver.prototype.onPlayStateChange_ = function() {
if (this.controlsTimerId_ != null) {
window.clearTimeout(this.controlsTimerId_);
}
if (this.video_.paused) {
this.pauseIcon_.textContent = 'pause';
} else {
this.pauseIcon_.textContent = 'play_arrow';
}
if (this.video_.paused && this.video_.readyState > 0) {
// Show controls.
this.controlsElement_.style.opacity = 1;
} else {
// Show controls for 3 seconds.
this.controlsElement_.style.opacity = 1;
this.controlsTimerId_ = window.setTimeout(function() {
this.controlsElement_.style.opacity = 0;
}.bind(this), 3000);
}
};
/**
* Initialize the receiver app by instantiating ShakaReceiver.
*/
function receiverAppInit() {
window.receiver = new ShakaReceiver();
window.receiver.init();
}
if (document.readyState == 'loading' ||
document.readyState == 'interactive') {
window.addEventListener('load', receiverAppInit);
} else {
receiverAppInit();
}
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| shaka-player.compiled.js | 18.69% | (810 / 4335) | 0.48% | (13 / 2734) | 0.62% | (6 / 971) | 92.96% | (251 / 270) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | 1 82 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 7 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | (function(){var g={};
(function(window){var k,aa=this;aa.md=!0;function m(a,b){var c=a.split("."),d=aa;c[0]in d||!d.execScript||d.execScript("var "+c[0]);for(var e;c.length&&(e=c.shift());)c.length||void 0===b?d[e]?d=d[e]:d=d[e]={}:d[e]=b}function ba(a){var b=p;function c(){}c.prototype=b.prototype;a.qd=b.prototype;a.prototype=new c;a.prototype.constructor=a;a.nd=function(a,c,f){return b.prototype[c].apply(a,Array.prototype.slice.call(arguments,2))}};/*
Copyright 2016 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
function ca(a){this.c=Math.exp(Math.log(.5)/a);this.b=this.a=0}function da(a,b,c){var d=Math.pow(a.c,b);c=c*(1-d)+d*a.a;isNaN(c)||(a.a=c,a.b+=b)}function ea(a){return a.a/(1-Math.pow(a.c,a.b))};function fa(){this.c=new ca(2);this.f=new ca(5);this.a=0;this.b=5E5}fa.prototype.setDefaultEstimate=function(a){this.b=a};fa.prototype.getBandwidthEstimate=function(){return 128E3>this.a?this.b:Math.min(ea(this.c),ea(this.f))};function ga(){}function ha(){};function q(){this.h=null;this.f=!1;this.b=new fa;this.g={};this.a={};this.i=!1;this.c=null}m("shaka.abr.SimpleAbrManager",q);q.prototype.stop=function(){this.h=null;this.f=!1;this.g={};this.a={};this.c=null};q.prototype.stop=q.prototype.stop;q.prototype.init=function(a){this.h=a};q.prototype.init=q.prototype.init;
q.prototype.chooseStreams=function(a){for(var b in a)this.g[b]=a[b];b={};if("audio"in a){var c=ia(this);c?(b.audio=c,this.a.audio=c):delete this.a.audio}"video"in a&&((c=ja(this))?(b.video=c,this.a.video=c):delete this.a.video);"text"in a&&(b.text=a.text.streams[0]);this.c=Date.now();return b};q.prototype.chooseStreams=q.prototype.chooseStreams;q.prototype.enable=function(){this.f=!0};q.prototype.enable=q.prototype.enable;q.prototype.disable=function(){this.f=!1};q.prototype.disable=q.prototype.disable;
q.prototype.segmentDownloaded=function(a,b,c){var d=this.b;b-=a;16E3>c||(a=8E3*c/b,b/=1E3,d.a+=c,da(d.c,b,a),da(d.f,b,a));if(null!=this.c&&this.f)a:{if(!this.i){if(!(128E3<=this.b.a))break a;this.i=!0}else if(8E3>Date.now()-this.c)break a;c={};if(d=ia(this))c.audio=d,this.a.audio=d;if(d=ja(this))c.video=d,this.a.video=d;this.c=Date.now();this.b.getBandwidthEstimate();this.h(c)}};q.prototype.segmentDownloaded=q.prototype.segmentDownloaded;q.prototype.getBandwidthEstimate=function(){return this.b.getBandwidthEstimate()};
q.prototype.getBandwidthEstimate=q.prototype.getBandwidthEstimate;q.prototype.setDefaultEstimate=function(a){this.b.setDefaultEstimate(a)};q.prototype.setDefaultEstimate=q.prototype.setDefaultEstimate;function ia(a){a=a.g.audio;if(!a)return null;a=ka(a);return a[Math.floor(a.length/2)]}
function ja(a){var b=a.g.video;if(!b)return null;var b=ka(b),c=a.a.audio,c=c&&c.bandwidth||0;a=a.b.getBandwidthEstimate();for(var d=b[0],e=0;e<b.length;++e){var f=b[e];if(f.bandwidth){var g=((e+1<b.length?b[e+1]:{bandwidth:Infinity}).bandwidth+c)/.85;a>=(f.bandwidth+c)/.95&&a<=g&&(d=f)}}return d}function ka(a){return a.streams.slice(0).filter(function(a){return a.allowedByApplication&&a.allowedByKeySystem}).sort(function(a,c){return a.bandwidth-c.bandwidth})};function t(a,b){var c=b||{},d;for(d in c)this[d]=c[d];this.defaultPrevented=this.cancelable=this.bubbles=!1;this.timeStamp=window.performance&&window.performance.now?window.performance.now():Date.now();this.type=a;this.isTrusted=!1;this.target=this.currentTarget=null;this.a=!1}t.prototype.preventDefault=function(){};t.prototype.stopImmediatePropagation=function(){this.a=!0};t.prototype.stopPropagation=function(){};var la="ended play playing pause pausing ratechange seeked seeking timeupdate volumechange".split(" "),ma="buffered currentTime duration ended loop muted paused playbackRate seeking videoHeight videoWidth volume".split(" "),na=["loop","playbackRate"],oa=["pause","play"],pa="adaptation buffering emsg error loading unloading texttrackvisibility trackschanged".split(" "),qa="drmInfo getConfiguration getManifestUri getPlaybackRate getTracks getStats isBuffering isInProgress isLive isTextTrackVisible keySystem seekRange".split(" "),
ra=[["getConfiguration","configure"]],sa=[["isTextTrackVisible","setTextTrackVisibility"]],ta="addTextTrack cancelTrickPlay configure resetConfiguration selectTrack setTextTrackVisibility trickPlay".split(" "),ua=["load","unload"];
function va(a){return JSON.stringify(a,function(a,c){if("manager"!=a&&"function"!=typeof c){if(c instanceof Event||c instanceof t){var b={},e;for(e in c){var f=c[e];f&&"object"==typeof f||e in Event||(b[e]=f)}return b}if(c instanceof TimeRanges)for(b={__type__:"TimeRanges",length:c.length,start:[],end:[]},e=0;e<c.length;++e)b.start.push(c.start(e)),b.end.push(c.end(e));else b="number"==typeof c?isNaN(c)?"NaN":isFinite(c)?c:0>c?"-Infinity":"Infinity":c;return b}})}
function wa(a){return JSON.parse(a,function(a,c){return"NaN"==c?NaN:"-Infinity"==c?-Infinity:"Infinity"==c?Infinity:c&&"object"==typeof c&&"TimeRanges"==c.__type__?xa(c):c})}function xa(a){return{length:a.length,start:function(b){return a.start[b]},end:function(b){return a.end[b]}}};function v(a,b,c){this.category=a;this.code=b;this.data=Array.prototype.slice.call(arguments,2)}m("shaka.util.Error",v);v.prototype.toString=function(){return"shaka.util.Error "+JSON.stringify(this,null," ")};v.Category={NETWORK:1,TEXT:2,MEDIA:3,MANIFEST:4,STREAMING:5,DRM:6,PLAYER:7,CAST:8,STORAGE:9};
v.Code={UNSUPPORTED_SCHEME:1E3,BAD_HTTP_STATUS:1001,HTTP_ERROR:1002,TIMEOUT:1003,MALFORMED_DATA_URI:1004,UNKNOWN_DATA_URI_ENCODING:1005,INVALID_TEXT_HEADER:2E3,INVALID_TEXT_CUE:2001,UNABLE_TO_DETECT_ENCODING:2003,BAD_ENCODING:2004,INVALID_XML:2005,INVALID_TTML:2006,INVALID_MP4_TTML:2007,INVALID_MP4_VTT:2008,BUFFER_READ_OUT_OF_BOUNDS:3E3,JS_INTEGER_OVERFLOW:3001,EBML_OVERFLOW:3002,EBML_BAD_FLOATING_POINT_SIZE:3003,MP4_SIDX_WRONG_BOX_TYPE:3004,MP4_SIDX_INVALID_TIMESCALE:3005,MP4_SIDX_TYPE_NOT_SUPPORTED:3006,
WEBM_CUES_ELEMENT_MISSING:3007,WEBM_EBML_HEADER_ELEMENT_MISSING:3008,WEBM_SEGMENT_ELEMENT_MISSING:3009,WEBM_INFO_ELEMENT_MISSING:3010,WEBM_DURATION_ELEMENT_MISSING:3011,WEBM_CUE_TRACK_POSITIONS_ELEMENT_MISSING:3012,WEBM_CUE_TIME_ELEMENT_MISSING:3013,MEDIA_SOURCE_OPERATION_FAILED:3014,MEDIA_SOURCE_OPERATION_THREW:3015,VIDEO_ERROR:3016,QUOTA_EXCEEDED_ERROR:3017,UNABLE_TO_GUESS_MANIFEST_TYPE:4E3,DASH_INVALID_XML:4001,DASH_NO_SEGMENT_INFO:4002,DASH_EMPTY_ADAPTATION_SET:4003,DASH_EMPTY_PERIOD:4004,DASH_WEBM_MISSING_INIT:4005,
DASH_UNSUPPORTED_CONTAINER:4006,DASH_PSSH_BAD_ENCODING:4007,DASH_NO_COMMON_KEY_SYSTEM:4008,DASH_MULTIPLE_KEY_IDS_NOT_SUPPORTED:4009,DASH_CONFLICTING_KEY_IDS:4010,UNPLAYABLE_PERIOD:4011,RESTRICTIONS_CANNOT_BE_MET:4012,NO_PERIODS:4014,DASH_DUPLICATE_REPRESENTATION_ID:4018,INVALID_STREAMS_CHOSEN:5005,NO_RECOGNIZED_KEY_SYSTEMS:6E3,REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE:6001,FAILED_TO_CREATE_CDM:6002,FAILED_TO_ATTACH_TO_VIDEO:6003,INVALID_SERVER_CERTIFICATE:6004,FAILED_TO_CREATE_SESSION:6005,FAILED_TO_GENERATE_LICENSE_REQUEST:6006,
LICENSE_REQUEST_FAILED:6007,LICENSE_RESPONSE_REJECTED:6008,ENCRYPTED_CONTENT_WITHOUT_DRM_INFO:6010,NO_LICENSE_SERVER_GIVEN:6012,OFFLINE_SESSION_REMOVED:6013,EXPIRED:6014,LOAD_INTERRUPTED:7E3,CAST_API_UNAVAILABLE:8E3,NO_CAST_RECEIVERS:8001,ALREADY_CASTING:8002,UNEXPECTED_CAST_ERROR:8003,CAST_CANCELED_BY_USER:8004,CAST_CONNECTION_TIMED_OUT:8005,CAST_RECEIVER_APP_UNAVAILABLE:8006,INDEXED_DB_NOT_SUPPORTED:9E3,INDEXED_DB_ERROR:9001,OPERATION_ABORTED:9002,REQUESTED_ITEM_NOT_FOUND:9003,MALFORMED_OFFLINE_URI:9004,
CANNOT_STORE_LIVE_OFFLINE:9005,STORE_ALREADY_IN_PROGRESS:9006,NO_INIT_DATA_FOR_OFFLINE:9007};function w(){var a,b,c=new Promise(function(c,e){a=c;b=e});c.resolve=a;c.reject=b;return c};function ya(a,b,c,d,e){this.C=a;this.l=b;this.A=c;this.B=d;this.s=e;this.f=this.j=this.h=!1;this.v="";this.a=this.i=null;this.b={video:{},player:{}};this.m=0;this.c={};this.g=null}k=ya.prototype;k.o=function(){za(this);this.a&&(this.a.stop(function(){},function(){}),this.a=null);this.B=this.A=this.l=null;this.f=this.j=this.h=!1;this.g=this.c=this.b=this.a=this.i=null;return Promise.resolve()};k.O=function(){return this.f};k.cb=function(){return this.v};
k.init=function(){if(window.chrome&&chrome.cast&&chrome.cast.isAvailable){delete window.__onGCastApiAvailable;this.h=!0;this.l();var a=new chrome.cast.SessionRequest(this.C),a=new chrome.cast.ApiConfig(a,this.kc.bind(this),this.rc.bind(this),"origin_scoped");chrome.cast.initialize(a,function(){},function(){})}else window.__onGCastApiAvailable=function(a){a&&this.init()}.bind(this)};k.fb=function(a){this.i=a;this.f&&Aa(this,{type:"appData",appData:this.i})};
k.cast=function(a){if(!this.h)return Promise.reject(new v(8,8E3));if(!this.j)return Promise.reject(new v(8,8001));if(this.f)return Promise.reject(new v(8,8002));this.g=new w;chrome.cast.requestSession(this.ab.bind(this,a),this.ub.bind(this));return this.g};
k.get=function(a,b){if("video"==a){if(0<=oa.indexOf(b))return this.Fb.bind(this,a,b)}else if("player"==a){if(0<=ta.indexOf(b))return this.Fb.bind(this,a,b);if(0<=ua.indexOf(b))return this.Gc.bind(this,a,b);if(0<=qa.indexOf(b))return this.Cb.bind(this,a,b)}return this.Cb(a,b)};k.set=function(a,b,c){this.b[a][b]=c;Aa(this,{type:"set",targetName:a,property:b,value:c})};
k.ab=function(a,b){this.a=b;this.a.addUpdateListener(this.vb.bind(this));this.a.addMessageListener("urn:x-cast:com.google.shaka.v2",this.lc.bind(this));this.vb();Aa(this,{type:"init",initState:a,appData:this.i});this.g.resolve()};k.ub=function(a){var b=8003;switch(a.code){case "cancel":b=8004;break;case "timeout":b=8005;break;case "receiver_unavailable":b=8006}this.g.reject(new v(8,b,a))};k.Cb=function(a,b){return this.b[a][b]};
k.Fb=function(a,b){Aa(this,{type:"call",targetName:a,methodName:b,args:Array.prototype.slice.call(arguments,2)})};k.Gc=function(a,b){var c=Array.prototype.slice.call(arguments,2),d=new w,e=this.m.toString();this.m++;this.c[e]=d;Aa(this,{type:"asyncCall",targetName:a,methodName:b,args:c,id:e});return d};k.kc=function(a){var b=this.s();this.g=new w;this.ab(b,a)};k.rc=function(a){this.j="available"==a;this.l()};
k.vb=function(){var a=this.a?"connected"==this.a.status:!1;if(this.f&&!a){this.B();for(var b in this.b)this.b[b]={};za(this)}this.v=(this.f=a)?this.a.receiver.friendlyName:"";this.l()};function za(a){for(var b in a.c){var c=a.c[b];delete a.c[b];c.reject(new v(7,7E3))}}
k.lc=function(a,b){var c=wa(b);switch(c.type){case "event":var d=c.targetName,e=c.event;this.A(d,new t(e.type,e));break;case "update":e=c.update;for(d in e){var c=this.b[d]||{},f;for(f in e[d])c[f]=e[d][f]}break;case "asyncComplete":if(d=c.id,f=c.error,c=this.c[d],delete this.c[d],c)if(f){d=new v(f.category,f.code);for(e in f)d[e]=f[e];c.reject(d)}else c.resolve()}};function Aa(a,b){var c=va(b);a.a.sendMessage("urn:x-cast:com.google.shaka.v2",c,function(){},ga)};function Ba(){this.a={}}k=Ba.prototype;k.push=function(a,b){this.a.hasOwnProperty(a)?this.a[a].push(b):this.a[a]=[b]};k.set=function(a,b){this.a[a]=b};k.has=function(a){return this.a.hasOwnProperty(a)};k.get=function(a){return(a=this.a[a])?a.slice():null};k.remove=function(a,b){var c=this.a[a];if(c)for(var d=0;d<c.length;++d)c[d]==b&&(c.splice(d,1),--d)};k.keys=function(){var a=[],b;for(b in this.a)a.push(b);return a};function x(){this.a=new Ba}x.prototype.o=function(){Ca(this);this.a=null;return Promise.resolve()};function y(a,b,c,d){b=new Da(b,c,d);a.a.push(c,b)}x.prototype.la=function(a,b){for(var c=this.a.get(b)||[],d=0;d<c.length;++d){var e=c[d];e.target==a&&(e.la(),this.a.remove(b,e))}};function Ca(a){var b=a.a,c=[],d;for(d in b.a)c.push.apply(c,b.a[d]);for(b=0;b<c.length;++b)c[b].la();a.a.a={}}function Da(a,b,c){this.target=a;this.type=b;this.a=c;this.target.addEventListener(b,c,!1)}
Da.prototype.la=function(){this.target&&(this.target.removeEventListener(this.type,this.a,!1),this.a=this.target=null)};function p(){this.Ba=new Ba;this.U=this}p.prototype.addEventListener=function(a,b){this.Ba.push(a,b)};p.prototype.removeEventListener=function(a,b){this.Ba.remove(a,b)};p.prototype.dispatchEvent=function(a){for(var b=this.Ba.get(a.type)||[],c=0;c<b.length;++c){a.target=this.U;a.currentTarget=this.U;var d=b[c];try{d.handleEvent?d.handleEvent(a):d.call(this,a)}catch(e){}if(a.a)break}return a.defaultPrevented};function z(a,b,c){p.call(this);this.c=a;this.b=b;this.h=this.f=this.g=this.i=this.j=null;this.a=new ya(c,this.Vc.bind(this),this.Wc.bind(this),this.Xc.bind(this),this.qb.bind(this));Ea(this)}ba(z);m("shaka.cast.CastProxy",z);z.prototype.o=function(){var a=[this.h?this.h.o():null,this.b?this.b.o():null,this.a?this.a.o():null];this.a=this.h=this.i=this.j=this.b=this.c=null;return Promise.all(a)};z.prototype.destroy=z.prototype.o;z.prototype.cc=function(){return this.j};z.prototype.getVideo=z.prototype.cc;
z.prototype.ac=function(){return this.i};z.prototype.getPlayer=z.prototype.ac;z.prototype.Qb=function(){return this.a?this.a.h&&this.a.j:!1};z.prototype.canCast=z.prototype.Qb;z.prototype.O=function(){return this.a?this.a.O():!1};z.prototype.isCasting=z.prototype.O;z.prototype.cb=function(){return this.a?this.a.cb():""};z.prototype.receiverName=z.prototype.cb;z.prototype.cast=function(){var a=this.qb();return this.a.cast(a).then(function(){return this.b.kb()}.bind(this))};z.prototype.cast=z.prototype.cast;
z.prototype.fb=function(a){this.a.fb(a)};z.prototype.setAppData=z.prototype.fb;z.prototype.cd=function(){var a=this.a;if(a.f){var b=a.s();chrome.cast.requestSession(a.ab.bind(a,b),a.ub.bind(a))}};z.prototype.suggestDisconnect=z.prototype.cd;
function Ea(a){a.a.init();a.h=new x;la.forEach(function(a){y(this.h,this.c,a,this.kd.bind(this))}.bind(a));pa.forEach(function(a){y(this.h,this.b,a,this.Cc.bind(this))}.bind(a));a.j={};for(var b in a.c)Object.defineProperty(a.j,b,{configurable:!1,enumerable:!0,get:a.jd.bind(a,b),set:a.ld.bind(a,b)});a.i={};for(b in a.b)Object.defineProperty(a.i,b,{configurable:!1,enumerable:!0,get:a.Bc.bind(a,b)});a.g=new p;a.g.U=a.j;a.f=new p;a.f.U=a.i}k=z.prototype;
k.qb=function(){var a={video:{},player:{},playerAfterLoad:{},manifest:this.b.pa,startTime:null};this.c.pause();na.forEach(function(b){a.video[b]=this.c[b]}.bind(this));this.c.ended||(a.startTime=this.c.currentTime);ra.forEach(function(b){var c=b[1];b=this.b[b[0]]();a.player[c]=b}.bind(this));sa.forEach(function(b){var c=b[1];b=this.b[b[0]]();a.playerAfterLoad[c]=b}.bind(this));return a};k.Vc=function(){this.dispatchEvent(new t("caststatuschanged"))};
k.Xc=function(){ra.forEach(function(a){var b=a[1];a=this.a.get("player",a[0])();this.b[b](a)}.bind(this));var a=this.a.get("player","getManifestUri")(),b=this.a.get("video","ended"),c=Promise.resolve(),d=this.c.autoplay,e=null;b||(e=this.a.get("video","currentTime"));a&&(this.c.autoplay=!1,c=this.b.load(a,e),c["catch"](function(a){this.b.dispatchEvent(new t("error",{detail:a}))}.bind(this)));var f={};na.forEach(function(a){f[a]=this.a.get("video",a)}.bind(this));c.then(function(){na.forEach(function(a){this.c[a]=
f[a]}.bind(this));sa.forEach(function(a){var b=a[1];a=this.a.get("player",a[0])();this.b[b](a)}.bind(this));this.c.autoplay=d;a&&this.c.play()}.bind(this))};
k.jd=function(a){if("addEventListener"==a)return this.g.addEventListener.bind(this.g);if("removeEventListener"==a)return this.g.removeEventListener.bind(this.g);if(this.a.O()&&!Object.keys(this.a.b.video).length){var b=this.c[a];if("function"!=typeof b)return b}return this.a.O()?this.a.get("video",a):(b=this.c[a],"function"==typeof b&&(b=b.bind(this.c)),b)};k.ld=function(a,b){this.a.O()?this.a.set("video",a,b):this.c[a]=b};k.kd=function(a){this.a.O()||this.g.dispatchEvent(new t(a.type,a))};
k.Bc=function(a){return"addEventListener"==a?this.f.addEventListener.bind(this.f):"removeEventListener"==a?this.f.removeEventListener.bind(this.f):"getNetworkingEngine"==a?this.b.rb.bind(this.b):this.a.O()&&!Object.keys(this.a.b.video).length&&0<=qa.indexOf(a)||!this.a.O()?(a=this.b[a],a.bind(this.b)):this.a.get("player",a)};k.Cc=function(a){this.a.O()||this.f.dispatchEvent(a)};k.Wc=function(a,b){this.a.O()&&("video"==a?this.g.dispatchEvent(b):"player"==a&&this.f.dispatchEvent(b))};function A(a,b,c){p.call(this);this.b=a;this.a=b;this.i={video:a,player:b};this.j=c||function(){};this.h=!1;this.c=!0;this.f=this.g=null;Fa(this)}ba(A);m("shaka.cast.CastReceiver",A);A.prototype.ec=function(){return this.h};A.prototype.isConnected=A.prototype.ec;A.prototype.fc=function(){return this.c};A.prototype.isIdle=A.prototype.fc;
A.prototype.o=function(){var a=this.a?this.a.o():Promise.resolve();null!=this.f&&window.clearTimeout(this.f);this.j=this.i=this.a=this.b=null;this.h=!1;this.c=!0;this.f=this.g=null;return a.then(function(){cast.receiver.CastReceiverManager.getInstance().stop()})};A.prototype.destroy=A.prototype.o;
function Fa(a){var b=cast.receiver.CastReceiverManager.getInstance();b.onSenderConnected=a.Ab.bind(a);b.onSenderDisconnected=a.Ab.bind(a);b.onSystemVolumeChanged=a.Xb.bind(a);a.g=b.getCastMessageBus("urn:x-cast:com.google.shaka.v2");a.g.onMessage=a.mc.bind(a);b.start();la.forEach(function(a){this.b.addEventListener(a,this.Db.bind(this,"video"))}.bind(a));pa.forEach(function(a){this.a.addEventListener(a,this.Db.bind(this,"player"))}.bind(a));cast.__platform__&&cast.__platform__.canDisplayType('video/mp4; codecs="avc1.640028"; width=3840; height=2160')?
a.a.gb(3840,2160):a.a.gb(1920,1080);a.a.addEventListener("loading",function(){this.c=!1;Ga(this)}.bind(a));a.b.addEventListener("playing",function(){this.c=!1;Ga(this)}.bind(a));a.a.addEventListener("unloading",function(){this.c=!0;Ga(this)}.bind(a));a.b.addEventListener("ended",function(){window.setTimeout(function(){this.b&&this.b.ended&&(this.c=!0,Ga(this))}.bind(this),5E3)}.bind(a))}k=A.prototype;k.Ab=function(){this.h=!!cast.receiver.CastReceiverManager.getInstance().getSenders().length;Ga(this)};
function Ga(a){Promise.resolve().then(function(){this.dispatchEvent(new t("caststatuschanged"))}.bind(a))}
function Ha(a,b,c){for(var d in b.player)a.a[d](b.player[d]);a.j(c);c=Promise.resolve();var e=a.b.autoplay;b.manifest&&(a.b.autoplay=!1,c=a.a.load(b.manifest,b.startTime),c["catch"](function(a){this.a.dispatchEvent(new t("error",{detail:a}))}.bind(a)));c.then(function(){for(var a in b.video){var c=b.video[a];this.b[a]=c}for(a in b.playerAfterLoad)c=b.playerAfterLoad[a],this.a[a](c);this.b.autoplay=e;b.manifest&&this.b.play()}.bind(a))}
k.Db=function(a,b){this.bb();Ia(this,{type:"event",targetName:a,event:b})};k.bb=function(){null!=this.f&&window.clearTimeout(this.f);this.f=window.setTimeout(this.bb.bind(this),500);var a={video:{},player:{}};ma.forEach(function(b){a.video[b]=this.b[b]}.bind(this));qa.forEach(function(b){a.player[b]=this.a[b]()}.bind(this));var b=cast.receiver.CastReceiverManager.getInstance().getSystemVolume();b&&(a.video.volume=b.level,a.video.muted=b.muted);Ia(this,{type:"update",update:a})};
k.Xb=function(){var a=cast.receiver.CastReceiverManager.getInstance().getSystemVolume();a&&Ia(this,{type:"update",update:{video:{volume:a.level,muted:a.muted}}});Ia(this,{type:"event",targetName:"video",event:{type:"volumechange"}})};
k.mc=function(a){var b=wa(a.data);switch(b.type){case "init":Ha(this,b.initState,b.appData);this.bb();break;case "appData":this.j(b.appData);break;case "set":var c=b.targetName,d=b.property,e=b.value;if("video"==c)if(b=cast.receiver.CastReceiverManager.getInstance(),"volume"==d){b.setSystemVolumeLevel(e);break}else if("muted"==d){b.setSystemVolumeMuted(e);break}this.i[c][d]=e;break;case "call":c=b.targetName;d=b.methodName;e=b.args;c=this.i[c];c[d].apply(c,e);break;case "asyncCall":c=b.targetName,
d=b.methodName,e=b.args,b=b.id,a=a.senderId,c=this.i[c],c[d].apply(c,e).then(this.Jb.bind(this,a,b,null),this.Jb.bind(this,a,b))}};k.Jb=function(a,b,c){Ia(this,{type:"asyncComplete",id:b,error:c},a)};function Ia(a,b,c){a.h&&(b=va(b),c?a.g.getCastChannel(c).send(b):a.g.broadcast(b))};function Ja(a,b){return a.reduce(function(a,b,e){return b["catch"](a.bind(null,e))}.bind(null,b),Promise.reject())}function B(a,b){return a.concat(b)}function C(){}function Ka(a){return null!=a}function La(a){return function(b){return b!=a}}function Ma(a,b,c){return c.indexOf(a)==b};function Na(a){return!a||!Object.keys(a).length}function F(a){return Object.keys(a).map(function(b){return a[b]})}function Oa(a,b){return Object.keys(a).reduce(function(c,d){c[d]=b(a[d],d);return c},{})}function Pa(a,b){return Object.keys(a).every(function(c){return b(c,a[c])})};function Qa(a){return window.btoa(String.fromCharCode.apply(null,a)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=*$/,"")}function Ra(a){a=window.atob(a.replace(/-/g,"+").replace(/_/g,"/"));for(var b=new Uint8Array(a.length),c=0;c<a.length;++c)b[c]=a.charCodeAt(c);return b}function Sa(a){for(var b=new Uint8Array(a.length/2),c=0;c<a.length;c+=2)b[c/2]=window.parseInt(a.substr(c,2),16);return b}
function Ta(a){for(var b="",c=0;c<a.length;++c){var d=a[c].toString(16);1==d.length&&(d="0"+d);b+=d}return b}function Ua(a,b){if(!a&&!b)return!0;if(!a||!b||a.length!=b.length)return!1;for(var c=0;c<a.length;++c)if(a[c]!=b[c])return!1;return!0};function Va(a,b){var c=G(a,b);return 1!=c.length?null:c[0]}function G(a,b){return Array.prototype.filter.call(a.childNodes,function(a){return a.tagName==b})}function Wa(a){var b=a.firstChild;return b&&b.nodeType==Node.TEXT_NODE?a.textContent.trim():null}function H(a,b,c,d){var e=null;a=a.getAttribute(b);null!=a&&(e=c(a));return null==e?void 0!==d?d:null:e}function Xa(a){if(!a)return null;a=Date.parse(a);return isNaN(a)?null:Math.floor(a/1E3)}
function I(a){if(!a)return null;a=/^P(?:([0-9]*)Y)?(?:([0-9]*)M)?(?:([0-9]*)D)?(?:T(?:([0-9]*)H)?(?:([0-9]*)M)?(?:([0-9.]*)S)?)?$/.exec(a);if(!a)return null;a=31536E3*Number(a[1]||null)+2592E3*Number(a[2]||null)+86400*Number(a[3]||null)+3600*Number(a[4]||null)+60*Number(a[5]||null)+Number(a[6]||null);return isFinite(a)?a:null}function Ya(a){var b=/([0-9]+)-([0-9]+)/.exec(a);if(!b)return null;a=Number(b[1]);if(!isFinite(a))return null;b=Number(b[2]);return isFinite(b)?{start:a,end:b}:null}
function Za(a){a=Number(a);return a%1?null:a}function $a(a){a=Number(a);return!(a%1)&&0<a?a:null}function bb(a){a=Number(a);return!(a%1)&&0<=a?a:null}function cb(a){var b;a=(b=a.match(/^(\d+)\/(\d+)$/))?Number(b[1]/b[2]):Number(a);return isNaN(a)?null:a};var db={"urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b":"org.w3.clearkey","urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed":"com.widevine.alpha","urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95":"com.microsoft.playready","urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb":"com.adobe.primetime"};
function eb(a,b){var c=fb(a),d=null,e=c.filter(function(a){return"urn:mpeg:dash:mp4protection:2011"==a.Ib?(d=a.init||d,!1):!0}),f=c.map(function(a){return a.keyId}).filter(Ka),g=null;if(0<f.length&&(g=f[0],f.some(La(g))))throw new v(4,4010);f=[];0<e.length?(f=gb(d,b,e),f.length||(f=[hb("",d)])):0<c.length&&(f=F(db).map(function(a){return hb(a,d)}));return{nb:g,od:d,drmInfos:f,pb:!0}}
function ib(a,b,c){var d=eb(a,b);if(c.pb){a=1==c.drmInfos.length&&!c.drmInfos[0].keySystem;b=!d.drmInfos.length;if(!c.drmInfos.length||a&&!b)c.drmInfos=d.drmInfos;c.pb=!1}else if(0<d.drmInfos.length&&(c.drmInfos=c.drmInfos.filter(function(a){return d.drmInfos.some(function(b){return b.keySystem==a.keySystem})}),!c.drmInfos.length))throw new v(4,4008);return d.nb||c.nb}
function hb(a,b){return{keySystem:a,licenseServerUri:"",distinctiveIdentifierRequired:!1,persistentStateRequired:!1,audioRobustness:"",videoRobustness:"",serverCertificate:null,initData:b||[],keyIds:[]}}function gb(a,b,c){return c.map(function(c){var d=db[c.Ib];return d?[hb(d,c.init||a)]:b(c.node)||[]}).reduce(B,[])}
function fb(a){return a.map(function(a){var b=a.getAttribute("schemeIdUri"),d=a.getAttribute("cenc:default_KID"),e=G(a,"cenc:pssh").map(Wa);if(!b)return null;b=b.toLowerCase();if(d&&(d=d.replace(/-/g,"").toLowerCase(),0<=d.indexOf(" ")))throw new v(4,4009);var f=[];try{f=e.map(function(a){return{initDataType:"cenc",initData:Ra(a)}})}catch(g){throw new v(4,4007);}return{node:a,Ib:b,keyId:d,init:0<f.length?f:null}}).filter(Ka)};var jb=/^(?:([^:/?#.]+):)?(?:\/\/(?:([^/?#]*)@)?([^/#?]*?)(?::([0-9]+))?(?=[/#?]|$))?([^?#]+)?(?:\?([^#]*))?(?:#(.*))?$/;function kb(a){var b;a instanceof kb?(lb(this,a.T),this.ma=a.ma,this.V=a.V,mb(this,a.ya),this.P=a.P,nb(this,ob(a.a)),this.da=a.da):a&&(b=String(a).match(jb))?(lb(this,b[1]||"",!0),this.ma=pb(b[2]||""),this.V=pb(b[3]||"",!0),mb(this,b[4]),this.P=pb(b[5]||"",!0),nb(this,b[6]||"",!0),this.da=pb(b[7]||"")):this.a=new qb(null)}k=kb.prototype;k.T="";k.ma="";k.V="";k.ya=null;k.P="";k.da="";
k.toString=function(){var a=[],b=this.T;b&&a.push(rb(b,sb,!0),":");if(b=this.V){a.push("//");var c=this.ma;c&&a.push(rb(c,sb,!0),"@");a.push(encodeURIComponent(b).replace(/%25([0-9a-fA-F]{2})/g,"%$1"));b=this.ya;null!=b&&a.push(":",String(b))}if(b=this.P)this.V&&"/"!=b.charAt(0)&&a.push("/"),a.push(rb(b,"/"==b.charAt(0)?tb:ub,!0));(b=this.a.toString())&&a.push("?",b);(b=this.da)&&a.push("#",rb(b,vb));return a.join("")};
k.resolve=function(a){var b=new kb(this);"data"===b.T&&(b=new kb);var c=!!a.T;c?lb(b,a.T):c=!!a.ma;c?b.ma=a.ma:c=!!a.V;c?b.V=a.V:c=null!=a.ya;var d=a.P;if(c)mb(b,a.ya);else if(c=!!a.P){if("/"!=d.charAt(0))if(this.V&&!this.P)d="/"+d;else{var e=b.P.lastIndexOf("/");-1!=e&&(d=b.P.substr(0,e+1)+d)}if(".."==d||"."==d)d="";else if(-1!=d.indexOf("./")||-1!=d.indexOf("/.")){for(var e=!d.lastIndexOf("/",0),d=d.split("/"),f=[],g=0;g<d.length;){var h=d[g++];"."==h?e&&g==d.length&&f.push(""):".."==h?((1<f.length||
1==f.length&&""!=f[0])&&f.pop(),e&&g==d.length&&f.push("")):(f.push(h),e=!0)}d=f.join("/")}}c?b.P=d:c=""!==a.a.toString();c?nb(b,ob(a.a)):c=!!a.da;c&&(b.da=a.da);return b};function lb(a,b,c){a.T=c?pb(b,!0):b;a.T&&(a.T=a.T.replace(/:$/,""))}function mb(a,b){if(b){b=Number(b);if(isNaN(b)||0>b)throw Error("Bad port number "+b);a.ya=b}else a.ya=null}function nb(a,b,c){b instanceof qb?a.a=b:(c||(b=rb(b,wb)),a.a=new qb(b))}function pb(a,b){return a?b?decodeURI(a):decodeURIComponent(a):""}
function rb(a,b,c){return"string"==typeof a?(a=encodeURI(a).replace(b,xb),c&&(a=a.replace(/%25([0-9a-fA-F]{2})/g,"%$1")),a):null}function xb(a){a=a.charCodeAt(0);return"%"+(a>>4&15).toString(16)+(a&15).toString(16)}var sb=/[#\/\?@]/g,ub=/[\#\?:]/g,tb=/[\#\?]/g,wb=/[\#\?@]/g,vb=/#/g;function qb(a){this.b=a||null}qb.prototype.a=null;qb.prototype.c=null;
qb.prototype.toString=function(){if(this.b)return this.b;if(!this.a)return"";var a=[],b;for(b in this.a)for(var c=encodeURIComponent(b),d=this.a[b],e=0;e<d.length;e++){var f=c;""!==d[e]&&(f+="="+encodeURIComponent(d[e]));a.push(f)}return this.b=a.join("&")};function ob(a){var b=new qb;b.b=a.b;if(a.a){var c={},d;for(d in a.a)c[d]=a.a[d].concat();b.a=c;b.c=a.c}return b};function yb(a,b,c){this.a=a;this.M=b;this.D=c}m("shaka.media.InitSegmentReference",yb);function J(a,b,c,d,e,f){this.position=a;this.startTime=b;this.endTime=c;this.a=d;this.M=e;this.D=f}m("shaka.media.SegmentReference",J);var zb=1/15;function Ab(a,b,c,d,e){null!==e&&(e=Math.round(e));var f={RepresentationID:b,Number:c,Bandwidth:d,Time:e};return a.replace(/\$(RepresentationID|Number|Bandwidth|Time)?(?:%0([0-9]+)d)?\$/g,function(a,b,c){if("$$"==a)return"$";var d=f[b];if(null==d)return a;"RepresentationID"==b&&c&&(c=void 0);a=d.toString();c=window.parseInt(c,10)||1;return Array(Math.max(0,c-a.length)+1).join("0")+a})}
function Bb(a,b,c){if(c.length){var d=c[0];d.startTime<=zb&&(c[0]=new J(d.position,0,d.endTime,d.a,d.M,d.D));a||(a=c[c.length-1],a.startTime>b||(c[c.length-1]=new J(a.position,a.startTime,b,a.a,a.M,a.D)))}}function K(a,b){if(!b.length)return a;var c=b.map(function(a){return new kb(a)});return a.map(function(a){return new kb(a)}).map(function(a){return c.map(a.resolve.bind(a))}).reduce(B,[]).map(function(a){return a.toString()})}
function Cb(a,b){var c=L(a,b,"timescale"),d=1;c&&(d=$a(c)||1);c=L(a,b,"duration");(c=$a(c||""))&&(c/=d);var e=L(a,b,"startNumber"),f=L(a,b,"presentationTimeOffset"),g=bb(e||"");if(null==e||null==g)g=1;var h=Db(a,b,"SegmentTimeline"),e=null;if(h){for(var e=d,l=Number(f),n=a.I.duration||Infinity,h=G(h,"S"),r=[],u=0,X=0;X<h.length;++X){var D=h[X],E=H(D,"t",bb),ab=H(D,"d",bb),D=H(D,"r",Za);null!=E&&(E-=l);if(!ab)break;E=null!=E?E:u;D=D||0;if(0>D)if(X+1<h.length){D=H(h[X+1],"t",bb);if(null==D)break;else if(E>=
D)break;D=Math.ceil((D-E)/ab)-1}else{if(Infinity==n)break;else if(E/e>=n)break;D=Math.ceil((n*e-E)/ab)-1}0<r.length&&E!=u&&(r[r.length-1].end=E/e);for(var Yc=0;Yc<=D;++Yc)u=E+ab,r.push({start:E/e,end:u/e,gd:E}),E=u}e=r}return{timescale:d,G:c,ja:g,presentationTimeOffset:Number(f)/d||0,lb:Number(f),w:e}}function L(a,b,c){return[b(a.u),b(a.R),b(a.L)].filter(Ka).map(function(a){return a.getAttribute(c)}).reduce(function(a,b){return a||b})}
function Db(a,b,c){return[b(a.u),b(a.R),b(a.L)].filter(Ka).map(function(a){return Va(a,c)}).reduce(function(a,b){return a||b})};function Eb(a){if(!a)return"";a=new Uint8Array(a);239==a[0]&&187==a[1]&&191==a[2]&&(a=a.subarray(3));a=escape(Fb(a));try{return decodeURIComponent(a)}catch(b){throw new v(2,2004);}}function Gb(a,b){if(!a)return"";if(a.byteLength%2)throw new v(2,2004);var c;if(a instanceof ArrayBuffer)c=a;else{var d=new Uint8Array(a.byteLength);d.set(new Uint8Array(a));c=d.buffer}var d=a.byteLength/2,e=new Uint16Array(d);c=new DataView(c);for(var f=0;f<d;f++)e[f]=c.getUint16(2*f,b);return Fb(e)}
function Hb(a){var b=new Uint8Array(a);if(239==b[0]&&187==b[1]&&191==b[2])return Eb(b);if(254==b[0]&&255==b[1])return Gb(b.subarray(2),!1);if(255==b[0]&&254==b[1])return Gb(b.subarray(2),!0);var c=function(a,b){return a.byteLength<=b||32<=a[b]&&126>=a[b]}.bind(null,b);if(b[0]||b[2]){if(!b[1]&&!b[3])return Gb(a,!0);if(c(0)&&c(1)&&c(2)&&c(3))return Eb(a)}else return Gb(a,!1);throw new v(2,2003);}
function Ib(a){a=unescape(encodeURIComponent(a));for(var b=new Uint8Array(a.length),c=0;c<a.length;++c)b[c]=a.charCodeAt(c);return b.buffer}function Fb(a){for(var b="",c=0;c<a.length;c+=16E3)b+=String.fromCharCode.apply(null,a.subarray(c,c+16E3));return b};function Jb(a){this.b=a;this.c=0==Kb;this.a=0}var Kb=1;function Lb(a){return a.a<a.b.byteLength}function Mb(a){try{var b=a.b.getUint8(a.a)}catch(c){Nb()}a.a+=1;return b}function Ob(a){try{var b=a.b.getUint16(a.a,a.c)}catch(c){Nb()}a.a+=2;return b}function M(a){try{var b=a.b.getUint32(a.a,a.c)}catch(c){Nb()}a.a+=4;return b}
function Pb(a){var b,c;try{a.c?(b=a.b.getUint32(a.a,!0),c=a.b.getUint32(a.a+4,!0)):(c=a.b.getUint32(a.a,!1),b=a.b.getUint32(a.a+4,!1))}catch(d){Nb()}if(2097151<c)throw new v(3,3001);a.a+=8;return c*Math.pow(2,32)+b}function Qb(a,b){a.a+b>a.b.byteLength&&Nb();var c=a.b.buffer.slice(a.a,a.a+b);a.a+=b;return new Uint8Array(c)}function N(a,b){a.a+b>a.b.byteLength&&Nb();a.a+=b}
function Rb(a){var b=a.a;try{for(;Lb(a)&&a.b.getUint8(a.a);)a.a+=1}catch(c){Nb()}b=a.b.buffer.slice(b,a.a);a.a+=1;return Eb(b)}function Nb(){throw new v(3,3E3);};function Sb(a,b){for(;Lb(b);){var c=b.a,d=M(b),e=M(b);1==d?d=Pb(b):d||(d=b.b.byteLength-c);if(e==a)return d;N(b,d-(b.a-c))}return-1}function Tb(a,b){for(var c=new Jb(new DataView(a)),d=[[1836019574,0],[1953653099,0],[1835297121,0],[1835626086,0],[1937007212,0],[1937011556,8],[b,0]],e=-1,f=0;f<d.length;f++){var g=d[f][1],e=Sb(d[f][0],c);if(-1==e)return-1;N(c,g)}return e};function Ub(a,b,c,d){var e=[];a=new Jb(new DataView(a));var f=Sb(1936286840,a);if(-1==f)throw new v(3,3004);var g=Mb(a);N(a,3);N(a,4);var h=M(a);if(!h)throw new v(3,3005);var l,n;g?(l=Pb(a),n=Pb(a)):(l=M(a),n=M(a));N(a,2);g=Ob(a);d=l-d;b=b+f+n;for(f=0;f<g;f++){l=M(a);n=(l&2147483648)>>>31;l&=2147483647;var r=M(a);N(a,4);if(1==n)throw new v(3,3006);e.push(new J(e.length,d/h,(d+r)/h,function(){return c},b,b+l-1));d+=r;b+=l}return e};function O(a){this.a=a}m("shaka.media.SegmentIndex",O);O.prototype.o=function(){this.a=null;return Promise.resolve()};O.prototype.destroy=O.prototype.o;O.prototype.find=function(a){for(var b=this.a.length-1;0<=b;--b){var c=this.a[b];if(a>=c.startTime&&a<c.endTime)return c.position}return null};O.prototype.find=O.prototype.find;O.prototype.get=function(a){if(!this.a.length)return null;a-=this.a[0].position;return 0>a||a>=this.a.length?null:this.a[a]};O.prototype.get=O.prototype.get;
O.prototype.Za=function(a){for(var b=[],c=0,d=0;c<this.a.length&&d<a.length;){var e=this.a[c],f=a[d];e.startTime<f.startTime?(b.push(e),c++):(e.startTime>f.startTime||(.1<Math.abs(e.endTime-f.endTime)?b.push(f):b.push(e),c++),d++)}for(;c<this.a.length;)b.push(this.a[c++]);if(b.length)for(c=b[b.length-1].position+1;d<a.length;)f=a[d++],f=new J(c++,f.startTime,f.endTime,f.a,f.M,f.D),b.push(f);else b=a;this.a=b};O.prototype.merge=O.prototype.Za;
O.prototype.Ta=function(a){for(var b=0;b<this.a.length&&!(this.a[b].endTime>a);++b);this.a.splice(0,b)};O.prototype.evict=O.prototype.Ta;function Vb(a){this.b=a;this.a=new Jb(a);Wb||(Wb=[new Uint8Array([255]),new Uint8Array([127,255]),new Uint8Array([63,255,255]),new Uint8Array([31,255,255,255]),new Uint8Array([15,255,255,255,255]),new Uint8Array([7,255,255,255,255,255]),new Uint8Array([3,255,255,255,255,255,255]),new Uint8Array([1,255,255,255,255,255,255,255])])}var Wb;
function Xb(a){var b;b=Yb(a);if(7<b.length)throw new v(3,3002);for(var c=0,d=0;d<b.length;d++)c=256*c+b[d];b=c;c=Yb(a);a:{for(d=0;d<Wb.length;d++)if(Ua(c,Wb[d])){d=!0;break a}d=!1}if(d)c=a.b.byteLength-a.a.a;else{if(8==c.length&&c[1]&224)throw new v(3,3001);for(var d=c[0]&(1<<8-c.length)-1,e=1;e<c.length;e++)d=256*d+c[e];c=d}c=a.a.a+c<=a.b.byteLength?c:a.b.byteLength-a.a.a;d=new DataView(a.b.buffer,a.b.byteOffset+a.a.a,c);N(a.a,c);return new Zb(b,d)}
function Yb(a){var b=Mb(a.a),c;for(c=1;8>=c&&!(b&1<<8-c);c++);if(8<c)throw new v(3,3002);var d=new Uint8Array(c);d[0]=b;for(b=1;b<c;b++)d[b]=Mb(a.a);return d}function Zb(a,b){this.id=a;this.a=b}function $b(a){if(8<a.a.byteLength)throw new v(3,3002);if(8==a.a.byteLength&&a.a.getUint8(0)&224)throw new v(3,3001);for(var b=0,c=0;c<a.a.byteLength;c++)var d=a.a.getUint8(c),b=256*b+d;return b};function ac(a,b,c,d,e,f){function g(){return e}var h=[];a=new Vb(a.a);for(var l=-1,n=-1;Lb(a.a);){var r=Xb(a);if(187==r.id){var u=bc(r);u&&(r=c*(u.hd-f),u=b+u.Fc,0<=l&&h.push(new J(h.length,l,r,g,n,u-1)),l=r,n=u)}}0<=l&&h.push(new J(h.length,l,d,g,n,null));return h}function bc(a){var b=new Vb(a.a);a=Xb(b);if(179!=a.id)throw new v(3,3013);a=$b(a);b=Xb(b);if(183!=b.id)throw new v(3,3012);for(var b=new Vb(b.a),c=0;Lb(b.a);){var d=Xb(b);if(241==d.id){c=$b(d);break}}return{hd:a,Fc:c}};function cc(a,b){var c=Db(a,b,"Initialization");if(!c)return null;var d=a.u.N,e=c.getAttribute("sourceURL");e&&(d=K(a.u.N,[e]));var e=0,f=null;if(c=H(c,"range",Ya))e=c.start,f=c.end;return new yb(function(){return d},e,f)}
function dc(a,b){var c=L(a,ec,"presentationTimeOffset"),d=cc(a,ec),e;e=Number(c);var f=a.u.contentType,g=a.u.mimeType.split("/")[1];if("text"!=f&&"mp4"!=g&&"webm"!=g)throw new v(4,4006);if("webm"==g&&!d)throw new v(4,4005);var f=Db(a,ec,"RepresentationIndex"),h=L(a,ec,"indexRange"),l=a.u.N,h=Ya(h||"");if(f){var n=f.getAttribute("sourceURL");n&&(l=K(a.u.N,[n]));h=H(f,"range",Ya,h)}if(!h)throw new v(4,4002);e=fc(a,b,d,l,h.start,h.end,g,e);return{createSegmentIndex:e.createSegmentIndex,findSegmentPosition:e.findSegmentPosition,
getSegmentReference:e.getSegmentReference,initSegmentReference:d,presentationTimeOffset:Number(c)||0}}
function fc(a,b,c,d,e,f,g,h){var l=a.presentationTimeline,n=a.I.start,r=a.I.duration,u=b,X=null;return{createSegmentIndex:function(){var b=[u(d,e,f),"webm"==g?u(c.a(),c.M,c.D):null];u=null;return Promise.all(b).then(function(b){var c,f,u=b[0];b=b[1]||null;if("mp4"==g)u=Ub(u,e,d,h);else{b=new Vb(new DataView(b));if(440786851!=Xb(b).id)throw new v(3,3008);f=Xb(b);if(408125543!=f.id)throw new v(3,3009);b=f.a.byteOffset;f=new Vb(f.a);for(c=null;Lb(f.a);){var D=Xb(f);if(357149030==D.id){c=D;break}}if(!c)throw new v(3,
3010);c=new Vb(c.a);D=1E6;for(f=null;Lb(c.a);){var E=Xb(c);if(2807729==E.id)D=$b(E);else if(17545==E.id)if(f=E,4==f.a.byteLength)f=f.a.getFloat32(0);else if(8==f.a.byteLength)f=f.a.getFloat64(0);else throw new v(3,3003);}if(null==f)throw new v(3,3011);c=D/=1E9;f*=D;u=Xb(new Vb(new DataView(u)));if(475249515!=u.id)throw new v(3,3007);u=ac(u,b,c,f,d,h)}Bb(a.Sa,r,u);l.Ha(n,u);X=new O(u)})},findSegmentPosition:function(a){return X.find(a)},getSegmentReference:function(a){return X.get(a)}}}
function ec(a){return a.Ia};function gc(a,b){var c=cc(a,hc),d;d=ic(a);var e=Cb(a,hc),f=e.ja;f||(f=1);var g=0;e.G?g=e.G*(f-1):e.w&&0<e.w.length&&(g=e.w[0].start);d={G:e.G,startTime:g,ja:f,presentationTimeOffset:e.presentationTimeOffset,w:e.w,wa:d};if(!d.G&&!d.w&&1<d.wa.length)throw new v(4,4002);if(!d.G&&!a.I.duration&&!d.w&&1==d.wa.length)throw new v(4,4002);if(d.w&&!d.w.length)throw new v(4,4002);f=e=null;a.L.id&&a.u.id&&(f=a.L.id+","+a.u.id,e=b[f]);g=jc(a.I.duration,d.ja,a.u.N,d);Bb(a.Sa,a.I.duration,g);e?(e.Za(g),e.Ta(a.presentationTimeline.ua()-
a.I.start)):(a.presentationTimeline.Ha(a.I.start,g),e=new O(g),f&&(b[f]=e));return{createSegmentIndex:Promise.resolve.bind(Promise),findSegmentPosition:e.find.bind(e),getSegmentReference:e.get.bind(e),initSegmentReference:c,presentationTimeOffset:d.presentationTimeOffset}}function hc(a){return a.$}
function jc(a,b,c,d){var e=d.wa.length;d.w&&d.w.length!=d.wa.length&&(e=Math.min(d.w.length,d.wa.length));for(var f=[],g=d.startTime,h=0;h<e;h++){var l=d.wa[h],n=K(c,[l.ic]),r;r=null!=d.G?g+d.G:d.w?d.w[h].end:g+a;f.push(new J(h+b,g,r,function(a){return a}.bind(null,n),l.start,l.end));g=r}return f}
function ic(a){return[a.u.$,a.R.$,a.L.$].filter(Ka).map(function(a){return G(a,"SegmentURL")}).reduce(function(a,c){return 0<a.length?a:c}).map(function(b){b.getAttribute("indexRange")&&!a.tb&&(a.tb=!0);var c=b.getAttribute("media");b=H(b,"mediaRange",Ya,{start:0,end:null});return{ic:c,start:b.start,end:b.end}})};function kc(a,b,c,d){var e=lc(a),f;f=Cb(a,mc);var g=L(a,mc,"media"),h=L(a,mc,"index");f={G:f.G,timescale:f.timescale,ja:f.ja,presentationTimeOffset:f.presentationTimeOffset,lb:f.lb,w:f.w,Ya:g,Fa:h};g=0+(f.Fa?1:0);g+=f.w?1:0;g+=f.G?1:0;if(!g)throw new v(4,4002);1!=g&&(f.Fa&&(f.w=null),f.G=null);if(!f.Fa&&!f.Ya)throw new v(4,4002);if(f.Fa){c=a.u.mimeType.split("/")[1];if("mp4"!=c&&"webm"!=c)throw new v(4,4006);if("webm"==c&&!e)throw new v(4,4005);d=Ab(f.Fa,a.u.id,null,a.bandwidth||null,null);d=K(a.u.N,
[d]);a=fc(a,b,e,d,0,null,c,f.presentationTimeOffset)}else f.G?(d||a.presentationTimeline.$a(f.G),a=nc(a,f)):(d=b=null,a.L.id&&a.u.id&&(d=a.L.id+","+a.u.id,b=c[d]),g=oc(a,f),Bb(a.Sa,a.I.duration,g),b?(b.Za(g),b.Ta(a.presentationTimeline.ua()-a.I.start)):(a.presentationTimeline.Ha(a.I.start,g),b=new O(g),d&&(c[d]=b)),a={createSegmentIndex:Promise.resolve.bind(Promise),findSegmentPosition:b.find.bind(b),getSegmentReference:b.get.bind(b)});return{createSegmentIndex:a.createSegmentIndex,findSegmentPosition:a.findSegmentPosition,
getSegmentReference:a.getSegmentReference,initSegmentReference:e,presentationTimeOffset:f.presentationTimeOffset}}function mc(a){return a.Ja}
function nc(a,b){var c=a.I.duration,d=b.G,e=b.ja,f=b.timescale,g=b.Ya,h=a.bandwidth||null,l=a.u.id,n=a.u.N;return{createSegmentIndex:Promise.resolve.bind(Promise),findSegmentPosition:function(a){return 0>a||c&&a>=c?null:Math.floor(a/d)},getSegmentReference:function(a){var b=a*d;return 0>b||c&&b>=c?null:new J(a,b,b+d,function(){var c=Ab(g,l,a+e,h,b*f);return K(n,[c])},0,null)}}}
function oc(a,b){for(var c=[],d=0;d<b.w.length;d++){var e=d+b.ja;c.push(new J(e,b.w[d].start,b.w[d].end,function(a,b,c,d,e,r){a=Ab(a,b,e,c,r);return K(d,[a]).map(function(a){return a.toString()})}.bind(null,b.Ya,a.u.id,a.bandwidth||null,a.u.N,e,b.w[d].gd+b.lb),0,null))}return c}function lc(a){var b=L(a,mc,"initialization");if(!b)return null;var c=a.u.id,d=a.bandwidth||null,e=a.u.N;return new yb(function(){var a=Ab(b,c,null,d,null);return K(e,[a])},0,null)};function P(a){this.f=!1;this.a=[];this.b=[];this.c=[];this.h=a||null}m("shaka.net.NetworkingEngine",P);P.RequestType={MANIFEST:0,SEGMENT:1,LICENSE:2};var pc={};P.registerScheme=function(a,b){pc[a]=b};P.unregisterScheme=function(a){delete pc[a]};P.prototype.Ec=function(a){this.b.push(a)};P.prototype.registerRequestFilter=P.prototype.Ec;P.prototype.fd=function(a){var b=this.b;a=b.indexOf(a);0<=a&&b.splice(a,1)};P.prototype.unregisterRequestFilter=P.prototype.fd;P.prototype.Tb=function(){this.b=[]};
P.prototype.clearAllRequestFilters=P.prototype.Tb;P.prototype.Eb=function(a){this.c.push(a)};P.prototype.registerResponseFilter=P.prototype.Eb;P.prototype.Ob=function(a){var b=this.c;a=b.indexOf(a);0<=a&&b.splice(a,1)};P.prototype.unregisterResponseFilter=P.prototype.Ob;P.prototype.Ub=function(){this.c=[]};P.prototype.clearAllResponseFilters=P.prototype.Ub;function qc(){return{maxAttempts:2,baseDelay:1E3,backoffFactor:2,fuzzFactor:.5,timeout:0}}
function rc(a,b){return{uris:a,method:"GET",body:null,headers:{},allowCrossSiteCredentials:!1,retryParameters:b}}P.prototype.o=function(){this.f=!0;this.b=[];this.c=[];for(var a=[],b=0;b<this.a.length;++b)a.push(this.a[b]["catch"](C));return Promise.all(a)};P.prototype.destroy=P.prototype.o;
P.prototype.request=function(a,b){if(this.f)return Promise.reject();for(var c=Date.now(),d=this.b,e=0;e<d.length;e++)try{d[e](a,b)}catch(l){return Promise.reject(l)}for(var e=b.retryParameters||{},d=e.maxAttempts||1,f=e.backoffFactor||2,g=null==e.baseDelay?1E3:e.baseDelay,h=this.g(a,b,0),e=1;e<d;e++)h=h["catch"](this.i.bind(this,a,b,g,e%b.uris.length)),g*=f;this.a.push(h);return h.then(function(b){0<=this.a.indexOf(h)&&this.a.splice(this.a.indexOf(h),1);var d=Date.now();this.h&&1==a&&this.h(c,d,b.data.byteLength);
return b}.bind(this))["catch"](function(a){0<=this.a.indexOf(h)&&this.a.splice(this.a.indexOf(h),1);return Promise.reject(a)}.bind(this))};P.prototype.request=P.prototype.request;P.prototype.g=function(a,b,c){if(this.f)return Promise.reject();var d=new kb(b.uris[c]),e=d.T;e||(e=location.protocol,e=e.slice(0,-1),lb(d,e),b.uris[c]=d.toString());return(e=pc[e])?e(b.uris[c],b).then(function(b){for(var c=this.c,d=0;d<c.length;d++)c[d](a,b);return b}.bind(this)):Promise.reject(new v(1,1E3,d))};
P.prototype.i=function(a,b,c,d){var e=new w,f=b.retryParameters||{};window.setTimeout(e.resolve,c*(1+(2*Math.random()-1)*(null==f.fuzzFactor?.5:f.fuzzFactor)));return e.then(this.g.bind(this,a,b,d))};var sc={},tc={};m("shaka.media.ManifestParser.registerParserByExtension",function(a,b){tc[a]=b});m("shaka.media.ManifestParser.registerParserByMime",function(a,b){sc[a]=b});function uc(){var a={},b;for(b in sc)a[b]=!0;for(b in tc)a[b]=!0;["application/dash+xml","application/x-mpegurl","application/vnd.apple.mpegurl","application/vnd.ms-sstr+xml"].forEach(function(b){a[b]=!!sc[b]});["mpd","m3u8","ism"].forEach(function(b){a[b]=!!tc[b]});return a}
function vc(a,b,c,d){var e=d;e||(d=(new kb(a)).P.split("/").pop().split("."),1<d.length&&(d=d.pop().toLowerCase(),e=tc[d]));if(e)return Promise.resolve(e);c=rc([a],c);c.method="HEAD";return b.request(0,c).then(function(b){(b=b.headers["content-type"])&&(b=b.toLowerCase());return(e=sc[b])?e:Promise.reject(new v(4,4E3,a))},function(a){return Promise.reject(a)})};function Q(a,b){this.j=a;this.i=b;this.c=this.a=Infinity;this.b=1;this.h=this.f=0;this.g=!0}m("shaka.media.PresentationTimeline",Q);Q.prototype.ea=function(){return this.a};Q.prototype.getDuration=Q.prototype.ea;Q.prototype.Aa=function(a){this.a=a};Q.prototype.setDuration=Q.prototype.Aa;Q.prototype.Kb=function(a){this.h=a};Q.prototype.setClockOffset=Q.prototype.Kb;Q.prototype.Mb=function(a){this.g=a};Q.prototype.setStatic=Q.prototype.Mb;Q.prototype.bc=function(){return this.c};
Q.prototype.getSegmentAvailabilityDuration=Q.prototype.bc;Q.prototype.Lb=function(a){this.c=a};Q.prototype.setSegmentAvailabilityDuration=Q.prototype.Lb;Q.prototype.Ha=function(a,b){b.length&&(this.b=b.reduce(function(a,b){return Math.max(a,b.endTime-b.startTime)},this.b),a||(this.f=Math.max(this.f,b[0].startTime)))};Q.prototype.notifySegments=Q.prototype.Ha;Q.prototype.$a=function(a){this.b=Math.max(this.b,a)};Q.prototype.notifyMaxSegmentDuration=Q.prototype.$a;
Q.prototype.S=function(){return Infinity==this.a&&!this.g};Q.prototype.isLive=Q.prototype.S;Q.prototype.fa=function(){return Infinity!=this.a&&!this.g};Q.prototype.isInProgress=Q.prototype.fa;Q.prototype.ta=function(){return Math.max(Math.min(this.f,this.Z()),this.ua())};Q.prototype.getEarliestStart=Q.prototype.ta;Q.prototype.ua=function(){return Infinity==this.c?0:Math.max(0,this.Z()-this.c)};Q.prototype.getSegmentAvailabilityStart=Q.prototype.ua;
Q.prototype.Z=function(){return this.S()||this.fa()?Math.min(Math.max(0,(Date.now()+this.h)/1E3-this.b-this.j),this.a):this.a};Q.prototype.getSegmentAvailabilityEnd=Q.prototype.Z;Q.prototype.Va=function(){return Math.max(0,this.Z()-(this.S()||this.fa()?this.i:0))};Q.prototype.getSeekRangeEnd=Q.prototype.Va;function wc(a,b,c){this.g=R[b];this.c=a;this.h=0;this.f=Infinity;this.a=this.b=null;this.i=c}var R={};m("shaka.media.TextEngine.registerParser",function(a,b){R[a]=b});m("shaka.media.TextEngine.unregisterParser",function(a){delete R[a]});function xc(a,b,c){return a>=b?null:new VTTCue(a,b,c)}m("shaka.media.TextEngine.makeCue",xc);wc.prototype.o=function(){this.c&&yc(this,function(){return!0});this.c=this.g=null;return Promise.resolve()};
function zc(a,b,c,d){var e=a.h;return Promise.resolve().then(function(){if(this.c){var a=this.g(b,e,c,d,this.i);if(null!=c&&null!=d){for(var g=0;g<a.length&&!(a[g].startTime>=this.f);++g)this.c.addCue(a[g]);null==this.b&&(this.b=c);this.a=Math.min(d,this.f)}}}.bind(a))}
wc.prototype.remove=function(a,b){return Promise.resolve().then(function(){this.c&&(yc(this,function(c){return c.startTime>=b||c.endTime<=a?!1:!0}),null==this.b||b<=this.b||a>=this.a||(a<=this.b&&b>=this.a?this.b=this.a=null:a<=this.b&&b<this.a?this.b=b:a>this.b&&b>=this.a&&(this.a=a)))}.bind(this))};function Ac(a,b){return null==a.a||a.a<b||b<a.b?0:a.a-b}function yc(a,b){for(var c=a.c.cues,d=[],e=0;e<c.length;++e)b(c[e])&&d.push(c[e]);for(e=0;e<d.length;++e)a.c.removeCue(d[e])};function Bc(a,b,c){return c==b||a>=Cc&&c==b.split("-")[0]||a>=Dc&&c.split("-")[0]==b.split("-")[0]?!0:!1}var Cc=1,Dc=2;function Ec(a){a=a.toLowerCase().split("-");var b=Fc[a[0]];b&&(a[0]=b);return a.join("-")}
var Fc={aar:"aa",abk:"ab",afr:"af",aka:"ak",alb:"sq",amh:"am",ara:"ar",arg:"an",arm:"hy",asm:"as",ava:"av",ave:"ae",aym:"ay",aze:"az",bak:"ba",bam:"bm",baq:"eu",bel:"be",ben:"bn",bih:"bh",bis:"bi",bod:"bo",bos:"bs",bre:"br",bul:"bg",bur:"my",cat:"ca",ces:"cs",cha:"ch",che:"ce",chi:"zh",chu:"cu",chv:"cv",cor:"kw",cos:"co",cre:"cr",cym:"cy",cze:"cs",dan:"da",deu:"de",div:"dv",dut:"nl",dzo:"dz",ell:"el",eng:"en",epo:"eo",est:"et",eus:"eu",ewe:"ee",fao:"fo",fas:"fa",fij:"fj",fin:"fi",fra:"fr",fre:"fr",
fry:"fy",ful:"ff",geo:"ka",ger:"de",gla:"gd",gle:"ga",glg:"gl",glv:"gv",gre:"el",grn:"gn",guj:"gu",hat:"ht",hau:"ha",heb:"he",her:"hz",hin:"hi",hmo:"ho",hrv:"hr",hun:"hu",hye:"hy",ibo:"ig",ice:"is",ido:"io",iii:"ii",iku:"iu",ile:"ie",ina:"ia",ind:"id",ipk:"ik",isl:"is",ita:"it",jav:"jv",jpn:"ja",kal:"kl",kan:"kn",kas:"ks",kat:"ka",kau:"kr",kaz:"kk",khm:"km",kik:"ki",kin:"rw",kir:"ky",kom:"kv",kon:"kg",kor:"ko",kua:"kj",kur:"ku",lao:"lo",lat:"la",lav:"lv",lim:"li",lin:"ln",lit:"lt",ltz:"lb",lub:"lu",
lug:"lg",mac:"mk",mah:"mh",mal:"ml",mao:"mi",mar:"mr",may:"ms",mkd:"mk",mlg:"mg",mlt:"mt",mon:"mn",mri:"mi",msa:"ms",mya:"my",nau:"na",nav:"nv",nbl:"nr",nde:"nd",ndo:"ng",nep:"ne",nld:"nl",nno:"nn",nob:"nb",nor:"no",nya:"ny",oci:"oc",oji:"oj",ori:"or",orm:"om",oss:"os",pan:"pa",per:"fa",pli:"pi",pol:"pl",por:"pt",pus:"ps",que:"qu",roh:"rm",ron:"ro",rum:"ro",run:"rn",rus:"ru",sag:"sg",san:"sa",sin:"si",slk:"sk",slo:"sk",slv:"sl",sme:"se",smo:"sm",sna:"sn",snd:"sd",som:"so",sot:"st",spa:"es",sqi:"sq",
srd:"sc",srp:"sr",ssw:"ss",sun:"su",swa:"sw",swe:"sv",tah:"ty",tam:"ta",tat:"tt",tel:"te",tgk:"tg",tgl:"tl",tha:"th",tib:"bo",tir:"ti",ton:"to",tsn:"tn",tso:"ts",tuk:"tk",tur:"tr",twi:"tw",uig:"ug",ukr:"uk",urd:"ur",uzb:"uz",ven:"ve",vie:"vi",vol:"vo",wel:"cy",wln:"wa",wol:"wo",xho:"xh",yid:"yi",yor:"yo",zha:"za",zho:"zh",zul:"zu"};function Gc(a,b,c){for(var d=0;d<a.length;++d)if(c(a[d],b))return d;return-1};function Hc(a){this.a=null;this.b=function(){this.a=null;a()}.bind(this)}Hc.prototype.cancel=function(){null!=this.a&&(clearTimeout(this.a),this.a=null)};function Ic(a){a.cancel();a.a=setTimeout(a.b,100)};function Jc(a,b,c){this.l=this.h=this.s=null;this.B=!1;this.b=null;this.f=new x;this.a=[];this.m=[];this.j=new w;this.H=a;this.i=null;this.g=function(a){this.j.reject(a);b(a)}.bind(this);this.A={};this.K=c;this.v=new Hc(this.Dc.bind(this));this.C=this.c=!1;this.j["catch"](function(){})}k=Jc.prototype;
k.o=function(){this.c=!0;var a=this.a.map(function(a){return(a.ha.close()||Promise.resolve())["catch"](C)});this.j.reject();this.f&&a.push(this.f.o());this.l&&a.push(this.l.setMediaKeys(null)["catch"](C));this.v&&this.v.cancel();this.f=this.l=this.h=this.s=this.b=this.v=null;this.a=[];this.m=[];this.g=this.i=this.H=null;return Promise.all(a)};k.configure=function(a){this.i=a};
k.init=function(a,b){var c={},d=[];this.C=b;this.m=a.offlineSessionIds;Kc(this,a,b||0<a.offlineSessionIds.length,c,d);return d.length?Lc(this,c,d):(this.B=!0,Promise.resolve())};
function Mc(a,b){if(!a.h)return y(a.f,b,"encrypted",function(){this.f.la(b,"encrypted");this.g(new v(6,6010))}.bind(a)),Promise.resolve();a.l=b;var c=a.l.setMediaKeys(a.h),c=c["catch"](function(a){return Promise.reject(new v(6,6003,a.message))}),d=null;a.b.serverCertificate&&(d=a.h.setServerCertificate(a.b.serverCertificate),d=d["catch"](function(a){return Promise.reject(new v(6,6004,a.message))}));return Promise.all([c,d]).then(function(){if(this.c)return Promise.reject();Nc(this);this.b.initData.length||
this.m.length||y(this.f,this.l,"encrypted",this.jc.bind(this))}.bind(a))["catch"](function(a){return this.c?Promise.resolve():Promise.reject(a)}.bind(a))}function Oc(a,b){return Promise.all(b.map(function(a){return Pc(this,a).then(function(a){if(a){for(var b=new w,c=0;c<this.a.length;c++)if(this.a[c].ha==a){this.a[c].Oa=b;break}return Promise.all([a.remove(),b])}}.bind(this))}.bind(a)))}
function Nc(a){var b=a.b?a.b.initData:[];b.forEach(function(a){Qc(this,a.initDataType,a.initData)}.bind(a));a.m.forEach(function(a){Pc(this,a)}.bind(a));b.length||a.m.length||a.j.resolve();return a.j}k.keySystem=function(){return this.b?this.b.keySystem:""};function Rc(a){return a.a.map(function(a){return a.ha.sessionId})}
function Kc(a,b,c,d,e){var f=Sc(a);b.periods.forEach(function(a){a.streamSets.forEach(function(a){"text"!=a.type&&(f&&(a.drmInfos=[f]),a.drmInfos.forEach(function(b){Tc(this,b);var f=d[b.keySystem];f||(f={audioCapabilities:[],videoCapabilities:[],distinctiveIdentifier:"optional",persistentState:c?"required":"optional",sessionTypes:[c?"persistent-license":"temporary"],label:b.keySystem,drmInfos:[]},d[b.keySystem]=f,e.push(b.keySystem));f.drmInfos.push(b);b.distinctiveIdentifierRequired&&(f.distinctiveIdentifier=
"required");b.persistentStateRequired&&(f.persistentState="required");var g="video"==a.type?f.videoCapabilities:f.audioCapabilities,h=("video"==a.type?b.videoRobustness:b.audioRobustness)||"";a.streams.forEach(function(a){var c=a.mimeType;a.codecs&&(c+='; codecs="'+a.codecs+'"');a.keyId&&b.keyIds.push(a.keyId);g.push({robustness:h,contentType:c})}.bind(this))}.bind(this)))}.bind(this))}.bind(a))}
function Lc(a,b,c){if(1==c.length&&""==c[0])return Promise.reject(new v(6,6E3));var d=new w,e=d;[!0,!1].forEach(function(a){c.forEach(function(c){var d=b[c];d.drmInfos.some(function(a){return!!a.licenseServerUri})==a&&(d.audioCapabilities.length||delete d.audioCapabilities,d.videoCapabilities.length||delete d.videoCapabilities,e=e["catch"](function(){return this.c?Promise.reject():navigator.requestMediaKeySystemAccess(c,[d])}.bind(this)))}.bind(this))}.bind(a));e=e["catch"](function(){return Promise.reject(new v(6,
6001))});e=e.then(function(a){if(this.c)return Promise.reject();var c=0<=navigator.userAgent.indexOf("Edge/"),d=a.getConfiguration();this.s=(d.audioCapabilities||[]).concat(d.videoCapabilities||[]).map(function(a){return a.contentType});c&&(this.s=null);c=b[a.keySystem];Uc(this,a.keySystem,c,c.drmInfos);return this.b.licenseServerUri?a.createMediaKeys():Promise.reject(new v(6,6012))}.bind(a)).then(function(a){if(this.c)return Promise.reject();this.h=a;this.B=!0}.bind(a))["catch"](function(a){if(this.c)return Promise.resolve();
this.s=this.b=null;return a instanceof v?Promise.reject(a):Promise.reject(new v(6,6002,a.message))}.bind(a));d.reject();return e}
function Tc(a,b){var c=b.keySystem;if(c){if(!b.licenseServerUri){var d=a.i.servers[c];d&&(b.licenseServerUri=d)}b.keyIds||(b.keyIds=[]);if(c=a.i.advanced[c])b.distinctiveIdentifierRequired||(b.distinctiveIdentifierRequired=c.distinctiveIdentifierRequired),b.persistentStateRequired||(b.persistentStateRequired=c.persistentStateRequired),b.videoRobustness||(b.videoRobustness=c.videoRobustness),b.audioRobustness||(b.audioRobustness=c.audioRobustness),b.serverCertificate||(b.serverCertificate=c.serverCertificate)}}
function Sc(a){if(Na(a.i.clearKeys))return null;var b=[],c=[],d;for(d in a.i.clearKeys){var e=a.i.clearKeys[d],f=Sa(d),e=Sa(e),f={kty:"oct",kid:Qa(f),k:Qa(e)};b.push(f);c.push(f.kid)}a=JSON.stringify({keys:b});c=JSON.stringify({kids:c});c=[{initData:new Uint8Array(Ib(c)),initDataType:"keyids"}];return{keySystem:"org.w3.clearkey",licenseServerUri:"data:application/json;base64,"+window.btoa(a),distinctiveIdentifierRequired:!1,persistentStateRequired:!1,audioRobustness:"",videoRobustness:"",serverCertificate:null,
initData:c,keyIds:[]}}function Uc(a,b,c,d){var e=[],f=[],g=[],h=[];Vc(d,e,f,g,h);a.b={keySystem:b,licenseServerUri:e[0],distinctiveIdentifierRequired:"required"==c.distinctiveIdentifier,persistentStateRequired:"required"==c.persistentState,audioRobustness:c.audioCapabilities?c.audioCapabilities[0].robustness:"",videoRobustness:c.videoCapabilities?c.videoCapabilities[0].robustness:"",serverCertificate:f[0],initData:g,keyIds:h}}
function Vc(a,b,c,d,e){function f(a,b){return a.initDataType==b.initDataType&&Ua(a.initData,b.initData)}a.forEach(function(a){-1==b.indexOf(a.licenseServerUri)&&b.push(a.licenseServerUri);a.serverCertificate&&-1==Gc(c,a.serverCertificate,Ua)&&c.push(a.serverCertificate);a.initData&&a.initData.forEach(function(a){-1==Gc(d,a,f)&&d.push(a)});if(a.keyIds)for(var g=0;g<a.keyIds.length;++g)-1==e.indexOf(a.keyIds[g])&&e.push(a.keyIds[g])})}
k.jc=function(a){for(var b=new Uint8Array(a.initData),c=0;c<this.a.length;++c)if(Ua(b,this.a[c].initData))return;Qc(this,a.initDataType,b)};
function Pc(a,b){var c;try{c=a.h.createSession("persistent-license")}catch(f){var d=new v(6,6005,f.message);a.g(d);return Promise.reject(d)}y(a.f,c,"message",a.Bb.bind(a));y(a.f,c,"keystatuseschange",a.wb.bind(a));var e={initData:null,ha:c,loaded:!1,Oa:null};a.a.push(e);return c.load(b).then(function(a){if(!this.c){if(a)return e.loaded=!0,this.a.every(function(a){return a.loaded})&&this.j.resolve(),c;this.a.splice(this.a.indexOf(e),1);this.g(new v(6,6013))}}.bind(a),function(a){this.c||(this.a.splice(this.a.indexOf(e),
1),this.g(new v(6,6005,a.message)))}.bind(a))}function Qc(a,b,c){var d;try{d=a.C?a.h.createSession("persistent-license"):a.h.createSession()}catch(e){a.g(new v(6,6005,e.message));return}y(a.f,d,"message",a.Bb.bind(a));y(a.f,d,"keystatuseschange",a.wb.bind(a));a.a.push({initData:c,ha:d,loaded:!1,Oa:null});d.generateRequest(b,c.buffer)["catch"](function(a){if(!this.c){for(var b=0;b<this.a.length;++b)if(this.a[b].ha==d){this.a.splice(b,1);break}this.g(new v(6,6006,a.message))}}.bind(a))}
k.Bb=function(a){for(var b=a.target,c,d=0;d<this.a.length;d++)if(this.a[d].ha==b){c=this.a[d].Oa;break}d=rc([this.b.licenseServerUri],this.i.retryParameters);d.body=a.message;d.method="POST";"com.microsoft.playready"==this.b.keySystem&&Wc(d);this.H.request(2,d).then(function(a){return this.c?Promise.reject():b.update(a.data).then(function(){c&&c.resolve()})}.bind(this),function(a){if(this.c)return Promise.resolve();a=new v(6,6007,a);this.g(a);c&&c.reject(a)}.bind(this))["catch"](function(a){if(this.c)return Promise.resolve();
a=new v(6,6008,a.message);this.g(a);c&&c.reject(a)}.bind(this))};function Wc(a){for(var b=Gb(a.body,!0),b=(new DOMParser).parseFromString(b,"application/xml"),c=b.getElementsByTagName("HttpHeader"),d=0;d<c.length;++d)a.headers[c[d].querySelector("name").textContent]=c[d].querySelector("value").textContent;a.body=Ra(b.querySelector("Challenge").textContent).buffer}
k.wb=function(a){a=a.target;var b;for(b=0;b<this.a.length&&this.a[b].ha!=a;++b);if(b!=this.a.length){var c=!1;a.keyStatuses.forEach(function(a,d){if("string"==typeof d){var e=d;d=a;a=e}if("com.microsoft.playready"==this.b.keySystem&&16==d.byteLength){var e=new DataView(d),f=e.getUint32(0,!0),l=e.getUint16(4,!0),n=e.getUint16(6,!0);e.setUint32(0,f,!1);e.setUint16(4,l,!1);e.setUint16(6,n,!1)}"com.microsoft.playready"==this.b.keySystem&&"status-pending"==a&&(a="usable");"status-pending"!=a&&(this.a[b].loaded=
!0,this.a.every(function(a){return a.loaded})&&this.j.resolve());"expired"==a&&(c=!0);e=Ta(new Uint8Array(d));this.A[e]=a}.bind(this));var d=a.expiration-Date.now();(0>d||c&&1E3>d)&&!this.a[b].Oa&&(this.a.splice(b,1),a.close());Ic(this.v)}};k.Dc=function(){Pa(this.A,function(a,b){return"expired"==b})&&this.g(new v(6,6014));this.K(this.A)};
function Xc(){var a=[],b=[{contentType:'video/mp4; codecs="avc1.42E01E"'},{contentType:'video/webm; codecs="vp8"'}],c=[{videoCapabilities:b,persistentState:"required",sessionTypes:["persistent-license"]},{videoCapabilities:b}],d={};"org.w3.clearkey com.widevine.alpha com.microsoft.playready com.apple.fps.2_0 com.apple.fps.1_0 com.apple.fps com.adobe.primetime".split(" ").forEach(function(b){var e=navigator.requestMediaKeySystemAccess(b,c).then(function(a){var c=a.getConfiguration().sessionTypes;d[b]=
{persistentState:c?0<=c.indexOf("persistent-license"):!1};return a.createMediaKeys()})["catch"](function(){d[b]=null});a.push(e)});return Promise.all(a).then(function(){return d})};function Zc(a){return!a||1==a.length&&1E-6>a.end(0)-a.start(0)?null:a.length?a.end(a.length-1):null}function $c(a,b){var c=0;if(!a||1==a.length&&1E-6>a.end(0)-a.start(0))return c;var d=!1,e=1E-4;b||(e=.25);for(var f=0;f<a.length;++f)if(b+e>=a.start(f)&&b<a.end(f))c+=a.end(f)-b,d=!0;else if(d&&.04>=a.start(f)-a.end(f-1))c+=a.end(f)-a.start(f),c+=a.start(f)-a.end(f-1);else if(0<f&&b+e<a.start(f)&&b+e>=a.end(f-1))if(.04>=a.start(f)-b)c+=a.end(f)-b,d=!0;else break;else d=!1;return c}
function ad(a,b,c){var d=$c(a,b);d||(d=$c(a,b+c))&&(d+=c);return d};function bd(a,b,c){this.f=a;this.F=b;this.i=c;this.c={};this.b=null;this.a={};this.g=new x;this.h=!1}
function cd(){var a={};'video/mp4; codecs="avc1.42E01E",video/mp4; codecs="avc3.42E01E",video/mp4; codecs="hvc1.1.6.L93.90",audio/mp4; codecs="mp4a.40.2",audio/mp4; codecs="ac-3",audio/mp4; codecs="ec-3",video/webm; codecs="vp8",video/webm; codecs="vp9",video/webm; codecs="av1",audio/webm; codecs="vorbis",audio/webm; codecs="opus",video/mp2t; codecs="avc1.42E01E",video/mp2t; codecs="avc3.42E01E",video/mp2t; codecs="hvc1.1.6.L93.90",video/mp2t; codecs="mp4a.40.2",video/mp2t; codecs="ac-3",video/mp2t; codecs="ec-3",video/mp2t; codecs="mp4a.40.2",text/vtt,application/mp4; codecs="wvtt",application/ttml+xml,application/mp4; codecs="stpp"'.split(",").forEach(function(b){a[b]=!!R[b]||
MediaSource.isTypeSupported(b);var c=b.split(";")[0];a[c]=a[c]||a[b]});return a}k=bd.prototype;k.o=function(){this.h=!0;var a=[],b;for(b in this.a){var c=this.a[b],d=c[0];this.a[b]=c.slice(0,1);d&&a.push(d.p["catch"](C));for(d=1;d<c.length;++d)c[d].p["catch"](C),c[d].p.reject()}this.b&&a.push(this.b.o());return Promise.all(a).then(function(){this.g.o();this.b=this.i=this.F=this.f=this.g=null;this.c={};this.a={}}.bind(this))};
k.init=function(a,b){for(var c in a){var d=a[c];"text"==c?this.b=new wc(this.i,d,b):(d=this.F.addSourceBuffer(d),y(this.g,d,"error",this.$c.bind(this,c)),y(this.g,d,"updateend",this.xa.bind(this,c)),this.c[c]=d,this.a[c]=[])}};function dd(a,b){var c;"text"==b?c=a.b.b:(c=ed(a,b),c=!c||1==c.length&&1E-6>c.end(0)-c.start(0)?null:1==c.length&&0>c.start(0)?0:c.length?c.start(0):null);return c}
function fd(a,b,c,d){"text"==b?(b=Ac(a.b,c),!b&&d&&(b=Ac(a.b,c+d))&&(b+=d)):(a=ed(a,b),b=ad(a,c,d||0));return b}function ed(a,b){try{return a.c[b].buffered}catch(c){return null}}function gd(a,b,c,d,e){return"text"==b?zc(a.b,c,d,e):hd(a,b,a.Zc.bind(a,b,c))}k.remove=function(a,b,c){return"text"==a?this.b.remove(b,c):hd(this,a,this.Gb.bind(this,a,b,c))};function id(a,b){return"text"==b?a.b.remove(0,Infinity):hd(a,b,a.Gb.bind(a,b,0,a.F.duration))}
function jd(a,b,c){return"text"==b?(a.b.h=c,Promise.resolve()):hd(a,b,a.Pc.bind(a,b,c))}function kd(a,b,c){return"text"==b?(a.b.f=c,Promise.resolve()):Promise.all([hd(a,b,a.Pb.bind(a,b)),hd(a,b,a.Nc.bind(a,b,c))])}k.endOfStream=function(a){return ld(this,function(){a?this.F.endOfStream(a):this.F.endOfStream()}.bind(this))};k.Aa=function(a){return ld(this,function(){this.F.duration=a}.bind(this))};k.ea=function(){return this.F.duration};k.Zc=function(a,b){this.c[a].appendBuffer(b)};
k.Gb=function(a,b,c){c<=b?this.xa(a):this.c[a].remove(b,c)};k.Pb=function(a){var b=this.c[a].appendWindowEnd;this.c[a].abort();this.c[a].appendWindowEnd=b;this.xa(a)};k.Yb=function(a){this.f.currentTime-=.001;this.xa(a)};k.Pc=function(a,b){this.c[a].timestampOffset=b;this.xa(a)};k.Nc=function(a,b){this.c[a].appendWindowEnd=b+.04;this.xa(a)};k.$c=function(a){this.a[a][0].p.reject(new v(3,3014,this.f.error?this.f.error.code:0))};k.xa=function(a){var b=this.a[a][0];b&&(b.p.resolve(),nd(this,a))};
function hd(a,b,c){if(a.h)return Promise.reject();c={start:c,p:new w};a.a[b].push(c);if(1==a.a[b].length)try{c.start()}catch(d){"QuotaExceededError"==d.name?c.p.reject(new v(3,3017,b)):c.p.reject(new v(3,3015,d)),nd(a,b)}return c.p}
function ld(a,b){if(a.h)return Promise.reject();var c=[],d;for(d in a.c){var e=new w,f={start:function(a){a.resolve()}.bind(null,e),p:e};a.a[d].push(f);c.push(e);1==a.a[d].length&&f.start()}return Promise.all(c).then(function(){var a,c;try{b()}catch(l){c=Promise.reject(new v(3,3015,l))}for(a in this.c)nd(this,a);return c}.bind(a),function(){return Promise.reject()}.bind(a))}function nd(a,b){a.a[b].shift();var c=a.a[b][0];if(c)try{c.start()}catch(d){c.p.reject(new v(3,3015,d)),nd(a,b)}};function od(a,b,c){var d=!1;a.streamSets.forEach(function(a){a.streams.forEach(function(e){var f=e.allowedByApplication;e.allowedByApplication=!0;if("video"==a.type){if(e.width<b.minWidth||e.width>b.maxWidth||e.width>c.width||e.height<b.minHeight||e.height>b.maxHeight||e.height>c.height||e.width*e.height<b.minPixels||e.width*e.height>b.maxPixels||e.bandwidth<b.minVideoBandwidth||e.bandwidth>b.maxVideoBandwidth)e.allowedByApplication=!1}else"audio"==a.type&&(e.bandwidth<b.minAudioBandwidth||e.bandwidth>
b.maxAudioBandwidth)&&(e.allowedByApplication=!1);f!=e.allowedByApplication&&(d=!0)})});return d}
function pd(a,b,c){var d="",e=null;a&&a.B&&(d=a.keySystem(),e=a.s);for(a=0;a<c.streamSets.length;++a){var f=c.streamSets[a];if(d&&f.drmInfos.length&&!f.drmInfos.some(function(a){return a.keySystem==d}))c.streamSets.splice(a,1),--a;else{for(var g=b[f.type],h=0;h<f.streams.length;++h){var l=f.streams[h],n=qd(l.mimeType,l.codecs);R[n]||MediaSource.isTypeSupported(n)?e&&l.encrypted&&0>e.indexOf(n)?(f.streams.splice(h,1),--h):!g||l.mimeType==g.mimeType&&l.codecs.split(".")[0]==g.codecs.split(".")[0]||
(f.streams.splice(h,1),--h):(f.streams.splice(h,1),--h)}f.streams.length||(c.streamSets.splice(a,1),--a)}}}function rd(a,b){return a.streamSets.map(function(a){var c=b?b[a.type]:null;return a.streams.filter(function(a){return a.allowedByApplication&&a.allowedByKeySystem}).map(function(b){return{id:b.id,active:c==b,type:a.type,bandwidth:b.bandwidth,language:a.language,kind:b.kind||null,width:b.width||null,height:b.height||null,frameRate:b.frameRate||void 0,codecs:b.codecs||null}})}).reduce(B,[])}
function sd(a,b){for(var c=0;c<a.streamSets.length;c++)for(var d=a.streamSets[c],e=0;e<d.streams.length;e++){var f=d.streams[e];if(f.id==b.id)return{stream:f,bd:d}}return null}function td(a){return a.streams.some(function(a){return a.allowedByApplication&&a.allowedByKeySystem})}
function ud(a,b,c){var d={};a.streamSets.forEach(function(a){!td(a)||a.type in d||(d[a.type]=a)});var e=0;a.streamSets.forEach(function(a){if(td(a)&&"video"==a.type){var b=vd(a);b>e?(e=b,d.video=a):b==e&&wd(a)<wd(d.video)&&(d.video=a)}});a.streamSets.forEach(function(a){td(a)&&a.primary&&(d[a.type].primary?wd(a)<wd(d[a.type])&&(d[a.type]=a):d[a.type]=a)});[Dc,Cc,0].forEach(function(e){a.streamSets.forEach(function(a){if(td(a)){var f;"audio"==a.type?f=b.preferredAudioLanguage:"text"==a.type&&(f=b.preferredTextLanguage);
if(f){f=Ec(f);var g=Ec(a.language);Bc(e,f,g)&&(a.language==d[a.type].language?wd(a)<wd(d[a.type])&&(d[a.type]=a):d[a.type]=a,c&&(c[a.type]=!0))}}})});return d}function wd(a){var b=0;if(!a||1>a.streams.length)return b;a.streams.forEach(function(a){b+=a.bandwidth});return b/a.streams.length}function vd(a){var b=0;if(!a)return b;a.streams.forEach(function(a){a.height>b&&(b=a.height)});return b}function qd(a,b){var c=a;b&&(c+='; codecs="'+b+'"');return c};function xd(){this.m=this.l=this.j=this.c=this.a=null;this.g=[];this.b=null;this.h=[];this.v=1;this.i={};this.s=0;this.f=null;this.La=this.La.bind(this)}m("shaka.dash.DashParser",xd);k=xd.prototype;k.configure=function(a){this.c=a};k.start=function(a,b,c,d,e){this.g=[a];this.a=b;this.j=c;this.l=d;this.m=e;return yd(this).then(function(){this.a&&zd(this,0);return this.b}.bind(this))};
k.stop=function(){this.a&&this.a.Ob(this.La);this.c=this.m=this.l=this.j=this.a=null;this.g=[];this.b=null;this.h=[];this.i={};null!=this.f&&(window.clearTimeout(this.f),this.f=null);return Promise.resolve()};function yd(a){return a.a.request(0,rc(a.g,a.c.retryParameters)).then(function(a){if(this.a)return Ad(this,a.data,a.uri)}.bind(a))}
function Ad(a,b,c){var d=Eb(b),e=new DOMParser,f=null;b=null;try{f=e.parseFromString(d,"text/xml")}catch(X){}f&&"MPD"==f.documentElement.tagName&&(b=f.documentElement);if(!b)throw new v(4,4001);c=[c];d=G(b,"Location").map(Wa).filter(Ka);0<d.length&&(c=a.g=d);d=G(b,"BaseURL").map(Wa);c=K(c,d);var g=H(b,"minBufferTime",I);a.s=H(b,"minimumUpdatePeriod",I,-1);var h=H(b,"availabilityStartTime",Xa),d=H(b,"timeShiftBufferDepth",I),l=H(b,"suggestedPresentationDelay",I),e=H(b,"maxSegmentDuration",I),f=b.getAttribute("type")||
"static",n;if(a.b)n=a.b.presentationTimeline;else{var r=Math.max(10,1.5*g);n=new Q(h,null!=l?l:r)}var h=Bd(a,{Sa:"static"!=f,presentationTimeline:n,L:null,I:null,R:null,u:null,bandwidth:void 0,tb:!1},c,b),l=h.duration,u=h.periods;n.Mb("static"==f);n.Aa(l||Infinity);n.Lb(null!=d?d:Infinity);n.$a(e||1);if(a.b)return Promise.resolve();b=G(b,"UTCTiming");d=n.S();h.ca&&a.a.Eb(a.La);return Cd(a,c,b,d).then(function(a){this.a&&(n.Kb(a),this.b={presentationTimeline:n,periods:u,offlineSessionIds:[],minBufferTime:g||
0})}.bind(a))}
function Bd(a,b,c,d){var e=H(d,"mediaPresentationDuration",I),f=!1,g=[],h=0;d=G(d,"Period");for(var l=0;l<d.length;l++){var n=d[l],h=H(n,"start",I,h),r=H(n,"duration",I);if(null==r)if(l+1!=d.length){var u=H(d[l+1],"start",I);null!=u&&(r=u-h)}else null!=e&&(r=e-h);u={start:h,duration:r,node:n,ca:!1};n=Dd(a,b,c,u);g.push(n);f=f||u.ca;u=b.L.id;a.h.every(La(u))&&(a.j(n),a.h.push(u),a.b&&a.b.periods.push(n));if(null==r){h=null;break}h+=r}return null!=e?{periods:g,duration:e,ca:f}:{periods:g,duration:h,
ca:f}}function Dd(a,b,c,d){b.L=Ed(d.node,null,c);b.I=d;b.L.id||(b.L.id="__shaka_period_"+d.start);a=G(d.node,"AdaptationSet").map(a.zc.bind(a,b)).filter(Ka);b=a.map(function(a){return a.Hc}).reduce(B,[]);c=b.filter(Ma);if(b.length!=c.length)throw new v(4,4018);if(!a.length)throw new v(4,4004);for(b=0;b<a.length;b++)a[b].ca&&(d.ca=!0);a=Fd(a);return{startTime:d.start,streamSets:a}}
k.zc=function(a,b){a.R=Ed(b,a.L,null);var c=!1,d=G(b,"Role"),e=void 0;"text"==a.R.contentType&&(e="subtitle");for(var f=0;f<d.length;f++){var g=d[f].getAttribute("schemeIdUri");if(null==g||"urn:mpeg:dash:role:2011"==g)switch(g=d[f].getAttribute("value"),g){case "main":c=!0;break;case "caption":case "subtitle":e=g}}var d=Gd(b),h=[];G(b,"SupplementalProperty").forEach(function(a){var b=a.getAttribute("schemeIdUri");("urn:mpeg:dash:adaptation-set-switching:2016"==b||"http://dashif.org/guidelines/AdaptationSetSwitching"==
b||"http://dashif.org/descriptor/AdaptationSetSwitching"==b)&&(a=a.getAttribute("value"))&&h.push.apply(h,a.split(","))});var l=null,n=!1;G(b,"EssentialProperty").forEach(function(a){"http://dashif.org/guidelines/trickmode"==a.getAttribute("schemeIdUri")?l=a.getAttribute("value"):n=!0});if(null!=l||n)return null;var f=G(b,"ContentProtection"),f=eb(f,this.c.dash.customScheme),g=Ec(b.getAttribute("lang")||"und"),r=G(b,"Representation"),e=r.map(this.Ac.bind(this,a,f,e,g)).filter(function(a){return!!a});
if(!e.length)throw new v(4,4003);a.R.contentType||(a.R.contentType=Hd(e[0].mimeType,e[0].codecs));r=r.map(function(a){return a.getAttribute("id")}).filter(Ka);return{id:a.R.id||"__fake__"+this.v++,contentType:a.R.contentType,language:g,hc:c,streams:e,drmInfos:f.drmInfos,dd:h,ca:d,Hc:r}};function Gd(a){if(Va(a,"InbandEventStream"))return!0;a=G(a,"Representation");var b;if(0<a.length)for(var c=0;c<a.length;c++)if(b=Va(a[c],"InbandEventStream"))return!0;return!1}
k.Ac=function(a,b,c,d,e){a.u=Ed(e,a.R,null);if(!Id(a.u))return null;a.bandwidth=H(e,"bandwidth",$a)||void 0;var f;f=this.Ic.bind(this);if(a.u.Ia)f=dc(a,f);else if(a.u.$)f=gc(a,this.i);else if(a.u.Ja)f=kc(a,f,this.i,!!this.b);else{var g=a.u.N,h=a.I.duration||0;f={createSegmentIndex:Promise.resolve.bind(Promise),findSegmentPosition:function(a){return 0<=a&&a<h?1:null},getSegmentReference:function(a){return 1!=a?null:new J(1,0,h,function(){return g},0,null)},initSegmentReference:null,presentationTimeOffset:0}}e=
G(e,"ContentProtection");e=ib(e,this.c.dash.customScheme,b);return{id:this.v++,createSegmentIndex:f.createSegmentIndex,findSegmentPosition:f.findSegmentPosition,getSegmentReference:f.getSegmentReference,initSegmentReference:f.initSegmentReference,presentationTimeOffset:f.presentationTimeOffset,mimeType:a.u.mimeType,codecs:a.u.codecs,frameRate:a.u.frameRate,bandwidth:a.bandwidth,width:a.u.width,height:a.u.height,kind:c,encrypted:0<b.drmInfos.length,keyId:e,language:d,allowedByApplication:!0,allowedByKeySystem:!0}};
k.Yc=function(){this.f=null;var a=Date.now();yd(this).then(function(){this.a&&zd(this,(Date.now()-a)/1E3)}.bind(this))["catch"](function(a){this.l(a);this.a&&zd(this,0)}.bind(this))};function zd(a,b){0>a.s||(a.f=window.setTimeout(a.Yc.bind(a),1E3*Math.max(Math.max(3,a.s)-b,0)))}
function Ed(a,b,c){b=b||{contentType:"",mimeType:"",codecs:"",frameRate:void 0};c=c||b.N;var d=G(a,"BaseURL").map(Wa),e=a.getAttribute("contentType")||b.contentType,f=a.getAttribute("mimeType")||b.mimeType,g=a.getAttribute("codecs")||b.codecs,h=H(a,"frameRate",cb)||b.frameRate;e||(e=Hd(f,g));return{N:K(c,d),Ia:Va(a,"SegmentBase")||b.Ia,$:Va(a,"SegmentList")||b.$,Ja:Va(a,"SegmentTemplate")||b.Ja,width:H(a,"width",bb)||b.width,height:H(a,"height",bb)||b.height,contentType:e,mimeType:f,codecs:g,frameRate:h,
id:a.getAttribute("id")}}
function Fd(a){var b={};a.forEach(function(a){b[a.id]=[a]});a.forEach(function(a){var c=b[a.id];a.dd.forEach(function(a){(a=b[a])&&a!=c&&(c.push.apply(c,a),a.forEach(function(a){b[a.id]=c}))})});var c=[],d=[];F(b).forEach(function(a){if(!(0<=d.indexOf(a))){d.push(a);var b=new Ba;a.forEach(function(a){b.push(a.contentType||"",a)});b.keys().forEach(function(a){var d=new Ba;b.get(a).forEach(function(a){d.push(a.language,a)});d.keys().forEach(function(b){var e=d.get(b);b={language:b,type:a,primary:e.some(function(a){return a.hc}),
drmInfos:e.map(function(a){return a.drmInfos}).reduce(B,[]),streams:e.map(function(a){return a.streams}).reduce(B,[])};c.push(b)})})}});return c}function Id(a){var b;b=0+(a.Ia?1:0);b+=a.$?1:0;b+=a.Ja?1:0;if(!b)return"text"==a.contentType||"application"==a.contentType?!0:!1;1!=b&&(a.Ia&&(a.$=null),a.Ja=null);return!0}
function Jd(a,b,c,d){b=K(b,[c]);b=rc(b,a.c.retryParameters);b.method=d;return a.a.request(0,b).then(function(a){if("HEAD"==d){if(!a.headers||!a.headers.date)return 0;a=a.headers.date}else a=Eb(a.data);a=Date.parse(a);return isNaN(a)?0:a-Date.now()})}
function Cd(a,b,c,d){c=c.map(function(a){return{scheme:a.getAttribute("schemeIdUri"),value:a.getAttribute("value")}});var e=a.c.dash.clockSyncUri;d&&!c.length&&e&&c.push({scheme:"urn:mpeg:dash:utc:http-head:2014",value:e});return Ja(c,function(a){var c=a.value;switch(a.scheme){case "urn:mpeg:dash:utc:http-head:2014":case "urn:mpeg:dash:utc:http-head:2012":return Jd(this,b,c,"HEAD");case "urn:mpeg:dash:utc:http-xsdate:2014":case "urn:mpeg:dash:utc:http-iso:2014":case "urn:mpeg:dash:utc:http-xsdate:2012":case "urn:mpeg:dash:utc:http-iso:2012":return Jd(this,
b,c,"GET");case "urn:mpeg:dash:utc:direct:2014":case "urn:mpeg:dash:utc:direct:2012":return a=Date.parse(c),isNaN(a)?0:a-Date.now();case "urn:mpeg:dash:utc:http-ntp:2014":case "urn:mpeg:dash:utc:ntp:2014":case "urn:mpeg:dash:utc:sntp:2014":return Promise.reject();default:return Promise.reject()}}.bind(a))["catch"](function(){return 0})}k.Ic=function(a,b,c){a=rc(a,this.c.retryParameters);null!=b&&(a.headers.Range="bytes="+b+"-"+(null!=c?c:""));return this.a.request(1,a).then(function(a){return a.data})};
k.La=function(a,b){if(1==a){var c=new Jb(new DataView(b.data)),d=Sb(1701671783,c);if(-1!=d){var e=c.a-8+d;N(c,4);d=Rb(c);if("urn:mpeg:dash:event:2012"==d)yd(this);else{var f=Rb(c),g=M(c),h=M(c),l=M(c),n=M(c),c=Qb(c,e-c.a);this.m(new t("emsg",{detail:{schemeIdUri:d,value:f,timescale:g,presentationTimeDelta:h,eventDuration:l,id:n,messageData:c}}))}}}};function Hd(a,b){return R[qd(a,b)]?"text":a.split("/")[0]}tc.mpd=xd;sc["application/dash+xml"]=xd;function S(a,b){var c=Eb(a),d=[],e=new DOMParser,f=null;try{f=e.parseFromString(c,"text/xml")}catch(r){throw new v(2,2005);}if(f){var g,h,l;if(e=f.getElementsByTagName("tt")[0])f=e.getAttribute("ttp:frameRate"),g=e.getAttribute("ttp:subFrameRate"),h=e.getAttribute("ttp:frameRateMultiplier"),l=e.getAttribute("ttp:tickRate"),c=e.getAttribute("xml:space")||"default";else throw new v(2,2006);if("default"!=c&&"preserve"!=c)throw new v(2,2005);c="default"==c;f=new Kd(f,g,h,l);g=S.b(e.getElementsByTagName("styling")[0]);
h=S.b(e.getElementsByTagName("layout")[0]);e=S.b(e.getElementsByTagName("body")[0]);for(l=0;l<e.length;l++){var n=S.c(e[l],b,f,g,h,c);n&&d.push(n)}}return d}m("shaka.media.TtmlTextParser",S);S.m=/^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/;S.v=/^(?:(\d{2,}):)?(\d{2}):(\d{2})$/;S.s=/^(?:(\d{2,}):)?(\d{2}):(\d{2}\.\d{2,})$/;S.A=/^(\d*\.?\d*)f$/;S.C=/^(\d*\.?\d*)t$/;S.B=/^(?:(\d*\.?\d*)h)?(?:(\d*\.?\d*)m)?(?:(\d*\.?\d*)s)?(?:(\d*\.?\d*)ms)?$/;S.l=/^(\d{1,2}|100)% (\d{1,2}|100)%$/;
S.Ka={left:"start",center:"center",right:"end",start:"start",end:"end"};S.Ra={left:"line-left",center:"center",right:"line-right"};S.b=function(a){var b=[];if(!a)return b;for(var c=a.childNodes,d=0;d<c.length;d++){var e="span"==c[d].nodeName&&"p"==a.nodeName;c[d].nodeType!=Node.ELEMENT_NODE||"br"==c[d].nodeName||e||(e=S.b(c[d]),b=b.concat(e))}b.length||b.push(a);return b};
S.h=function(a,b){for(var c=a.childNodes,d=0;d<c.length;d++)if("br"==c[d].nodeName&&0<d)c[d-1].textContent+="\n";else if(0<c[d].childNodes.length)S.h(c[d],b);else if(b){var e=c[d].textContent.trim(),e=e.replace(/\s+/g," ");c[d].textContent=e}};
S.c=function(a,b,c,d,e,f){if(!a.hasAttribute("begin")&&!a.hasAttribute("end")&&/^\s*$/.test(a.textContent))return null;S.h(a,f);f=S.a(a.getAttribute("begin"),c);var g=S.a(a.getAttribute("end"),c);c=S.a(a.getAttribute("dur"),c);var h=a.textContent;null==g&&null!=c&&(g=f+c);if(null==f||null==g)throw new v(2,2001);b=xc(f+b,g+b,h);if(!b)return null;e=S.i(a,"region",e);S.qa(b,a,e,d);return b};
S.qa=function(a,b,c,d){var e,f=S.f(b,c,d,"tts:extent");f&&(e=S.l.exec(f))&&(a.size=Number(e[1]));e=S.f(b,c,d,"tts:writingMode");f=!0;"tb"==e||"tblr"==e?a.vertical="lr":"tbrl"==e?a.vertical="rl":f=!1;if(e=S.f(b,c,d,"tts:origin"))if(e=S.l.exec(e))f?(a.position=Number(e[2]),a.line=Number(e[1])):(a.position=Number(e[1]),a.line=Number(e[2])),a.snapToLines=!1;if(b=S.f(b,c,d,"tts:textAlign"))a.align=b,"center"==b&&("center"!=a.align&&(a.align="middle"),a.position="auto"),a.positionAlign=S.Ra[b],a.lineAlign=
S.Ka[b]};S.f=function(a,b,c,d){for(var e=S.b(b),f=0;f<e.length;f++){var g=e[f].getAttribute(d);if(g)return g}e=S.i;return(a=e(b,"style",c)||e(a,"style",c))?a.getAttribute(d):null};S.i=function(a,b,c){if(!a||1>c.length)return null;var d=null;if(a=S.ra(a,b))for(b=0;b<c.length;b++)if(c[b].getAttribute("xml:id")==a){d=c[b];break}return d};S.ra=function(a,b){for(var c=null;a&&!(c=a.getAttribute(b));){var d=a.parentNode;if(d instanceof Element)a=d;else break}return c};
S.a=function(a,b){var c=null;S.m.test(a)?c=S.sa(b,a):S.v.test(a)?c=S.g(S.v,a):S.s.test(a)?c=S.g(S.s,a):S.A.test(a)?c=S.Ca(b,a):S.C.test(a)?c=S.Da(b,a):S.B.test(a)&&(c=S.g(S.B,a));return c};S.Ca=function(a,b){var c=S.A.exec(b);return Number(c[1])/a.frameRate};S.Da=function(a,b){var c=S.C.exec(b);return Number(c[1])/a.a};S.sa=function(a,b){var c=S.m.exec(b),d=Number(c[1]),e=Number(c[2]),f=Number(c[3]),g=Number(c[4]),g=g+(Number(c[5])||0)/a.b,f=f+g/a.frameRate;return f+60*e+3600*d};
S.g=function(a,b){var c=a.exec(b);return c&&""!=c[0]?(Number(c[4])||0)/1E3+(Number(c[3])||0)+60*(Number(c[2])||0)+3600*(Number(c[1])||0):null};function Kd(a,b,c,d){this.frameRate=Number(a)||30;this.b=Number(b)||1;this.a=Number(d);this.a||(this.a=a?this.frameRate*this.b:1);c&&(a=/^(\d+) (\d+)$/g.exec(c))&&(this.frameRate*=a[1]/a[2])}R["application/ttml+xml"]=S;function Ld(a,b){var c=new Jb(new DataView(a)),d=Sb(1835295092,c);if(-1!=d)return S(Qb(c,d-8).buffer,b);if(-1!=Tb(a,Ld.U))return[];throw new v(2,2007);}m("shaka.media.Mp4TtmlParser",Ld);Ld.U=1937010800;R['application/mp4; codecs="stpp"']=Ld;function Md(a){this.b=a;this.a=0}function Nd(a,b){var c;b.lastIndex=a.a;c=(c=b.exec(a.b))?{position:c.index,length:c[0].length,Kc:c}:null;if(a.a==a.b.length||!c||c.position!=a.a)return null;a.a+=c.length;return c.Kc}function Od(a){return a.a==a.b.length?null:(a=Nd(a,/[^ \t\n]*/gm))?a[0]:null};function T(a,b,c,d,e){a=Eb(a);a=a.replace(/\r\n|\r(?=[^\n]|$)/gm,"\n");a=a.split(/\n{2,}/m);if(!/^WEBVTT($|[ \t\n])/m.test(a[0]))throw new v(2,2E3);d=[];for(var f=1;f<a.length;f++){var g=a[f].split("\n");(g=T.c(g,b,c,e))&&d.push(g)}return d}m("shaka.media.VttTextParser",T);
T.c=function(a,b,c,d){if(1==a.length&&!a[0]||/^NOTE($|[ \t])/.test(a[0]))return null;var e=null;0>a[0].indexOf("--\x3e")&&(e=a[0],a.splice(0,1));var f=new Md(a[0]),g=T.a(f),h=Nd(f,/[ \t]+--\x3e[ \t]+/g),l=T.a(f);if(null==g||!h||null==l)throw new v(2,2001);d?(g+=c,l+=c):(g+=b,l+=b);a=xc(g,l,a.slice(1).join("\n").trim());if(!a)return null;Nd(f,/[ \t]+/gm);for(b=Od(f);b;)T.j(a,b),Nd(f,/[ \t]+/gm),b=Od(f);null!=e&&(a.id=e);return a};
T.j=function(a,b){var c;if(c=/^align:(start|middle|center|end|left|right)$/.exec(b))a.align=c[1],"center"==c[1]&&"center"!=a.align&&(a.position="auto",a.align="middle");else if(c=/^vertical:(lr|rl)$/.exec(b))a.vertical=c[1];else if(c=/^size:(\d{1,2}|100)%$/.exec(b))a.size=Number(c[1]);else if(c=/^position:(\d{1,2}|100)%(?:,(line-left|line-right|center|start|end))?$/.exec(b))a.position=Number(c[1]),c[2]&&(a.positionAlign=c[2]);else if(c=/^line:(\d{1,2}|100)%(?:,(start|end|center))?$/.exec(b))a.snapToLines=
!1,a.line=Number(c[1]),c[2]&&(a.lineAlign=c[2]);else if(c=/^line:(-?\d+)(?:,(start|end|center))?$/.exec(b))a.snapToLines=!0,a.line=Number(c[1]),c[2]&&(a.lineAlign=c[2])};T.a=function(a){a=Nd(a,/(?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{3})/g);if(!a)return null;var b=Number(a[2]),c=Number(a[3]);return 59<b||59<c?null:Number(a[4])/1E3+c+60*b+3600*(Number(a[1])||0)};R["text/vtt"]=T;R['text/vtt; codecs="vtt"']=T;function U(a,b,c,d){var e=new Jb(new DataView(a)),f=Sb(1835295092,e);if(-1!=f)return U.Ba(Qb(e,f-8).buffer,b,c,d);if(-1!=Tb(a,U.pa))return[];throw new v(2,2008);}m("shaka.media.Mp4VttParser",U);U.Ba=function(a,b,c,d){a=new Jb(new DataView(a));c+=b;d+=b;for(b=[];Lb(a);){var e=Sb(U.oa,a);if(-1==e)break;(e=U.c(Qb(a,e-8).buffer,c,d))&&b.push(e)}return b};
U.c=function(a,b,c){a=new Jb(new DataView(a));for(var d,e,f;Lb(a);){var g=M(a),h=M(a),l=Eb(Qb(a,g-8).buffer);1==g&&Pb(a);switch(h){case U.K:d=l;break;case U.H:f=l;break;case U.Y:e=l}}if(!d)throw new v(2,2008);b=xc(b,c,d);if(!b)return null;f&&(b.id=f);if(e)for(e=new Md(e),f=Od(e);f;)T.j(b,f),Nd(e,/[ \t]+/gm),f=Od(e);return b};U.pa=2004251764;U.oa=1987343459;U.K=1885436268;U.H=1768187247;U.Y=1937011815;R['application/mp4; codecs="wvtt"']=U;function Pd(a,b,c,d,e,f){this.a=a;this.c=b;this.j=c;this.s=d;this.l=e;this.m=f;this.b=new x;this.g=!1;this.h=1;this.i=this.f=null;0<a.readyState?this.xb():y(this.b,a,"loadedmetadata",this.xb.bind(this));y(this.b,a,"ratechange",this.qc.bind(this));Qd(this)}k=Pd.prototype;k.o=function(){var a=this.b.o();this.b=null;Rd(this);null!=this.f&&(window.clearInterval(this.f),this.f=null);this.m=this.l=this.c=this.a=null;return a};function Sd(a){return 0<a.a.readyState?Td(a,a.a.currentTime):Ud(a)}
function Ud(a){return a.s?Td(a,a.s):Infinity>a.c.ea()?a.c.ta():Math.max(a.c.Va(),a.c.ta())}function Vd(a,b){b!=a.g&&(a.g=b,Wd(a,a.h),a.l(b))}function Qd(a){Rd(a);a.i=window.setTimeout(a.uc.bind(a),250)}function Rd(a){a.i&&(window.clearTimeout(a.i),a.i=null)}k.uc=function(){this.i=null;Qd(this);var a=ad(this.a.buffered,this.a.currentTime,.1),b=Zc(this.a.buffered)>=(this.c.S()?this.c.Z()-.1:this.a.duration-.1)||this.a.ended;this.g?(b||a>=this.j)&&Vd(this,!1):!b&&.5>a&&Vd(this,!0)};k.Ua=function(){return this.h};
function Wd(a,b){null!=a.f&&(window.clearInterval(a.f),a.f=null);a.h=b;a.a.playbackRate=a.g||0>b?0:b;!a.g&&0>b&&(a.f=window.setInterval(function(){this.a.currentTime+=b/4}.bind(a),250))}k.qc=function(){this.a.playbackRate!=(this.g||0>this.h?0:this.h)&&Wd(this,this.a.playbackRate)};
k.xb=function(){this.b.la(this.a,"loadedmetadata");var a=Ud(this);.001>Math.abs(this.a.currentTime-a)?(y(this.b,this.a,"seeking",this.zb.bind(this)),y(this.b,this.a,"playing",this.yb.bind(this))):(y(this.b,this.a,"seeking",this.sc.bind(this)),this.a.currentTime=a)};k.sc=function(){this.b.la(this.a,"seeking");y(this.b,this.a,"seeking",this.zb.bind(this));y(this.b,this.a,"playing",this.yb.bind(this))};k.zb=function(){var a=this.a.currentTime,b=Xd(this,a);.001<Math.abs(b-a)?Yd(this,a,b):this.m()};
k.yb=function(){var a=this.a.currentTime,b=Xd(this,a);.001<Math.abs(b-a)&&Yd(this,a,b)};function Xd(a,b){var c=a.c,d=c.ta(),e=c.Z();if(!c.S()||Infinity==c.c)return b<d?d:b>e?e:b;c=d+1;d=c+a.j;return b>=d&&b<=e||$c(a.a.buffered,b)&&b>=c&&b<=e?b:b>e?e:e<d&&b>=c&&b<=e?b:Math.min(d+2,e)}function Yd(a,b,c){a.a.currentTime=c;var d=0,e=function(){!this.a||10<=d++||this.a.currentTime!=b||(this.a.currentTime=c,setTimeout(e,100))}.bind(a);setTimeout(e,100)}
function Td(a,b){var c=a.c.ta();if(b<c)return c;c=a.c.Z();return b>c?c:b};function Zd(a,b,c,d,e,f,g,h,l){this.m=a;this.f=b;this.U=c;this.a=d;this.H=e;this.v=f;this.j=g;this.A=h||null;this.B=l||null;this.g=null;this.i=1;this.C=Promise.resolve();this.h=[];this.l={};this.b={};this.c=this.s=this.K=!1}Zd.prototype.o=function(){for(var a in this.b)$d(this.b[a]);this.g=this.b=this.l=this.h=this.B=this.A=this.j=this.v=this.H=this.C=this.a=this.U=this.f=this.m=null;this.c=!0;return Promise.resolve()};
Zd.prototype.configure=function(a){this.g=a;this.m.j=this.i*Math.max(this.a.minBufferTime||0,this.g.rebufferingGoal)};Zd.prototype.init=function(){var a=this.H(this.a.periods[ae(this,Sd(this.m))]);return Na(a)?Promise.reject(new v(5,5005)):be(this,a).then(function(){this.A&&this.A()}.bind(this))};function ce(a){return a.a.periods[ae(a,Sd(a.m))]}function de(a){return Oa(a.b,function(a){return a.stream})}function ee(a,b){var c={};c.text=b;return be(a,c)}
function fe(a,b,c,d){var e=a.b[b];!e&&"text"==b&&a.g.ignoreTextStreamFailures?ee(a,c):e&&(b=a.h[ge(a,c)])&&b.za&&(b=a.l[c.id])&&b.za&&e.stream!=c&&(e.stream=c,e.Ma=!0,d&&(e.ba?e.Qa=!0:e.ga?(e.na=!0,e.Qa=!0):($d(e),he(a,e,!0))))}function ie(a){var b=Sd(a.m);if(!Object.keys(a.b).every(function(a){return 0<fd(this.f,a,b)}.bind(a)))for(var c in a.b){var d=a.b[c];d.ba||d.na||(d.ga?d.na=!0:null==dd(a.f,c)?null==d.aa&&je(a,d,0):($d(d),he(a,d,!1)))}}
function be(a,b,c){var d=ae(a,Sd(a.m)),e=Oa(b,function(a){return qd(a.mimeType,a.codecs)});a.f.init(e,a.g.useRelativeCueTimestamps);ke(a);e=F(b);return le(a,e).then(function(){if(!this.c)for(var a in b){var e=b[a];this.b[a]||(this.b[a]={stream:e,type:a,va:null,W:null,Ma:!0,Ga:d,endOfStream:!1,ga:!1,aa:null,na:!1,Qa:!1,ba:!1,eb:!1,Xa:!1,Hb:c||0},je(this,this.b[a],0))}}.bind(a))}
function me(a,b){var c=a.h[b];if(c)return c.J;c={J:new w,za:!1};a.h[b]=c;var d=a.a.periods[b].streamSets.map(function(a){return a.streams}).reduce(B,[]);a.C=a.C.then(function(){if(!this.c)return le(this,d)}.bind(a)).then(function(){this.c||(this.h[b].J.resolve(),this.h[b].za=!0)}.bind(a))["catch"](function(a){this.c||(this.h[b].J.reject(),delete this.h[b],this.j(a))}.bind(a));return c.J}
function le(a,b){for(var c=[],d=0;d<b.length;++d){var e=b[d],f=a.l[e.id];f?c.push(f.J):(a.l[e.id]={J:new w,za:!1},c.push(e.createSegmentIndex()))}return Promise.all(c).then(function(){if(!this.c)for(var a=0;a<b.length;++a){var c=this.l[b[a].id];c.za||(c.J.resolve(),c.za=!0)}}.bind(a))["catch"](function(a){if(!this.c)return this.l[e.id].J.reject(),delete this.l[e.id],Promise.reject(a)}.bind(a))}function ke(a){var b=a.a.presentationTimeline.ea();Infinity>b?a.f.Aa(b):a.f.Aa(Math.pow(2,32))}
Zd.prototype.Y=function(a){if(!this.c&&!a.ga&&null!=a.aa&&!a.ba)if(a.aa=null,a.na)he(this,a,a.Qa);else{try{var b=ne(this,a);null!=b&&(je(this,a,b),a.Xa=!1)}catch(c){this.j(c);return}b=F(this.b);oe(this,a);b.every(function(a){return a.endOfStream})&&this.f.endOfStream()}};
function ne(a,b){var c=Sd(a.m),d,e=a.f;d=b.type;d="text"==d?e.b.a:Zc(ed(e,d));var f=b.va&&b.W?a.a.periods[ge(a,b.va)].startTime+b.W.endTime:Math.max(c,b.Hb);b.Hb=0;var e=ge(a,b.stream),g=ae(a,f),h=fd(a.f,b.type,c,.1),l=Math.max(a.i*Math.max(a.a.minBufferTime||0,a.g.rebufferingGoal),a.i*a.g.bufferingGoal);if(f>=a.a.presentationTimeline.ea())return b.endOfStream=!0,null;b.endOfStream=!1;b.Ga=g;if(g!=e)return null;if(h>=l)return.5;b.W&&b.stream==b.va?(f=b.W.position+1,d=pe(a,b,e,f)):(f=b.W?b.stream.findSegmentPosition(Math.max(0,
a.a.periods[ge(a,b.va)].startTime+b.W.endTime-a.a.periods[e].startTime)):b.stream.findSegmentPosition(Math.max(0,(d||c)-a.a.periods[e].startTime)),null==f?d=null:(g=null,null==d&&(g=pe(a,b,e,Math.max(0,f-1))),d=g||pe(a,b,e,f)));if(!d)return 1;qe(a,b,c,e,d);return null}function pe(a,b,c,d){c=a.a.periods[c];b=b.stream.getSegmentReference(d);if(!b)return null;a=a.a.presentationTimeline;d=a.Z();return c.startTime+b.endTime<a.ua()||c.startTime+b.startTime>d?null:b}
function qe(a,b,c,d,e){var f=a.a.periods[d],g=b.stream,h=a.a.periods[d+1],l=null,l=h?h.startTime:a.a.presentationTimeline.ea();d=re(a,b,d,l);b.ga=!0;b.Ma=!1;h=se(a,e);Promise.all([d,h]).then(function(a){if(!this.c&&!this.s)return te(this,b,c,f,g,e,a[1])}.bind(a)).then(function(){this.c||this.s||(b.ga=!1,b.eb=!1,je(this,b,0),ue(this,g))}.bind(a))["catch"](function(a){this.c||this.s||(b.ga=!1,1001==a.code||1002==a.code||1003==a.code?"text"==b.type&&this.g.ignoreTextStreamFailures&&1001==a.code?delete this.b.text:
(this.j(a),je(this,b,4)):3017==a.code?ve(this,b,a):"text"==b.type&&this.g.ignoreTextStreamFailures?delete this.b.text:(b.Xa=!0,this.j(a)))}.bind(a))}function ve(a,b,c){if(!F(a.b).some(function(a){return a!=b&&a.eb})){var d=Math.round(100*a.i);if(20<d)a.i-=.2;else if(4<d)a.i-=.04;else{b.Xa=!0;a.s=!0;a.j(c);return}b.eb=!0}je(a,b,4)}
function re(a,b,c,d){if(!b.Ma)return Promise.resolve();c=jd(a.f,b.type,a.a.periods[c].startTime-b.stream.presentationTimeOffset);d=null!=d?kd(a.f,b.type,d):Promise.resolve();if(!b.stream.initSegmentReference)return Promise.all([c,d]);a=se(a,b.stream.initSegmentReference).then(function(a){if(!this.c)return gd(this.f,b.type,a,null,null)}.bind(a))["catch"](function(a){b.Ma=!0;return Promise.reject(a)});return Promise.all([c,d,a])}
function te(a,b,c,d,e,f,g){return we(a,b,c).then(function(){if(!this.c)return gd(this.f,b.type,g,f.startTime+d.startTime,f.endTime+d.startTime)}.bind(a)).then(function(){if(!this.c)return b.va=e,b.W=f,Promise.resolve()}.bind(a))}function we(a,b,c){var d=dd(a.f,b.type);if(null==d)return Promise.resolve();c=c-d-a.g.bufferBehind;return 0>=c?Promise.resolve():a.f.remove(b.type,d,d+c).then(function(){}.bind(a))}
function ue(a,b){if(!a.K&&(a.K=F(a.b).every(function(a){return"text"==a.type?!0:!a.na&&!a.ba&&a.W}),a.K)){var c=ge(a,b);a.h[c]||me(a,c).then(function(){this.v()}.bind(a))["catch"](C);for(c=0;c<a.a.periods.length;++c)me(a,c)["catch"](C);a.B&&a.B()}}
function oe(a,b){if(b.Ga!=ge(a,b.stream)){var c=b.Ga,d=F(a.b);d.every(function(a){return a.Ga==c})&&d.every(xe)&&me(a,c).then(function(){if(!this.c&&d.every(function(a){var b=ge(this,a.stream);return xe(a)&&a.Ga==c&&b!=c}.bind(this))){var a=this.a.periods[c],b=this.H(a),g;for(g in this.b)if(!b[g]&&"text"!=g){this.j(new v(5,5005));return}for(g in b)if(!this.b[g])if("text"==g)be(this,{text:b.text},a.startTime),delete b[g];else{this.j(new v(5,5005));return}for(g in this.b)(a=b[g])?(fe(this,g,a,!1),je(this,
this.b[g],0)):delete this.b[g];this.v()}}.bind(a))["catch"](C)}}function xe(a){return!a.ga&&null==a.aa&&!a.na&&!a.ba}function ae(a,b){for(var c=a.a.periods.length-1;0<c;--c)if(b>=a.a.periods[c].startTime)return c;return 0}function ge(a,b){for(var c=0;c<a.a.periods.length;++c)for(var d=a.a.periods[c],e=0;e<d.streamSets.length;++e)if(0<=d.streamSets[e].streams.indexOf(b))return c;return-1}
function se(a,b){var c=rc(b.a(),a.g.retryParameters);if(b.M||null!=b.D){var d="bytes="+b.M+"-";null!=b.D&&(d+=b.D);c.headers.Range=d}return a.U.request(1,c).then(function(a){return a.data})}function he(a,b,c){b.na=!1;b.Qa=!1;b.ba=!0;id(a.f,b.type).then(function(){if(!this.c&&c){var a=this.f,e=b.type;return"text"==e?Promise.resolve():hd(a,e,a.Yb.bind(a,e))}}.bind(a)).then(function(){this.c||(b.va=null,b.W=null,b.ba=!1,je(this,b,0))}.bind(a))}
function je(a,b,c){b.aa=window.setTimeout(a.Y.bind(a,b),1E3*c)}function $d(a){null!=a.aa&&(window.clearTimeout(a.aa),a.aa=null)};function ye(a){return new Promise(function(b){var c=a.split(":");if(2>c.length||"data"!=c[0])throw new v(1,1004,a);c=c.slice(1).join(":").split(",");if(2>c.length)throw new v(1,1004,a);var d=c[0],c=window.decodeURIComponent(c.slice(1).join(",")),d=d.split(";"),e=null;1<d.length&&(e=d[1]);if("base64"==e)c=Ra(c).buffer;else{if(e)throw new v(1,1005,a);c=Ib(c)}b({uri:a,data:c,headers:{"content-type":d[0]}})})}m("shaka.net.DataUriPlugin",ye);pc.data=ye;function ze(a,b){return new Promise(function(c,d){var e=new XMLHttpRequest;e.open(b.method,a,!0);e.responseType="arraybuffer";e.timeout=b.retryParameters.timeout;e.withCredentials=b.allowCrossSiteCredentials;e.onload=function(b){b=b.target;var e=b.getAllResponseHeaders().split("\r\n").reduce(function(a,b){var c=b.split(": ");a[c[0].toLowerCase()]=c.slice(1).join(": ");return a},{});if(200<=b.status&&299>=b.status&&202!=b.status)b.responseURL&&(a=b.responseURL),c({uri:a,data:b.response,headers:e,pd:!!e["x-shaka-from-cache"]});
else{var f=null;try{f=Hb(b.response)}catch(n){}d(new v(1,1001,a,b.status,f,e))}};e.onerror=function(){d(new v(1,1002,a))};e.ontimeout=function(){d(new v(1,1003,a))};for(var f in b.headers)e.setRequestHeader(f,b.headers[f]);e.send(b.body)})}m("shaka.net.HttpPlugin",ze);pc.http=ze;pc.https=ze;function Ae(){this.a=null;this.c=[];this.b={}}k=Ae.prototype;
k.init=function(a){if(!window.indexedDB)return Promise.reject(new v(9,9E3));var b=window.indexedDB.open("shaka_offline_db",1),c=new w;b.onupgradeneeded=function(b){b=b.target.result;for(var c in a)b.createObjectStore(c,{keyPath:a[c]})};b.onsuccess=function(a){this.a=a.target.result;c.resolve()}.bind(this);b.onerror=Be.bind(null,b,c);return c.then(function(){var b=Object.keys(a);return Promise.all(b.map(function(a){return Ce(this,a).then(function(b){this.b[a]=b}.bind(this))}.bind(this)))}.bind(this))};
k.o=function(){return Promise.all(this.c.map(function(a){try{a.transaction.abort()}catch(b){}return a.J["catch"](C)})).then(function(){this.a&&(this.a.close(),this.a=null)}.bind(this))};k.get=function(a,b){return De(this,a,"readonly",function(a){return a.get(b)})};k.forEach=function(a,b){return De(this,a,"readonly",function(a){return a.openCursor()},function(a){a&&(b(a.value),a["continue"]())})};function Ee(a,b,c){return De(a,b,"readwrite",function(a){return a.put(c)})}
k.remove=function(a,b){return De(this,a,"readwrite",function(a){return a["delete"](b)})};function Fe(a,b){var c=[];return De(a,"segment","readwrite",function(a){return a.openCursor()},function(a){if(a){if(b(a.value)){var d=a["delete"](),f=new w;d.onsuccess=f.resolve;d.onerror=Be.bind(null,d,f);c.push(f)}a["continue"]()}}).then(function(){return Promise.all(c)}).then(function(){return c.length})}
function Ce(a,b){var c=0;return De(a,b,"readonly",function(a){return a.openCursor(null,"prev")},function(a){a&&(c=a.key+1)}).then(function(){return c})}
function De(a,b,c,d,e){c=a.a.transaction([b],c);var f=d(c.objectStore(b)),g=new w;e&&(f.onsuccess=function(a){e(a.target.result)});f.onerror=Be.bind(null,f,g);var h={transaction:c,J:g};a.c.push(h);var l=function(){this.c.splice(this.c.indexOf(h),1)}.bind(a);c.oncomplete=function(){l();g.resolve(f.result)};c.onerror=function(a){l();Be(f,g,a)};return g}function Be(a,b,c){"AbortError"==a.error.name?b.reject(new v(9,9002)):b.reject(new v(9,9001,a.error));c.preventDefault()};var Ge={manifest:"key",segment:"key"};function He(a){return{offlineUri:"offline:"+a.key,originalManifestUri:a.originalManifestUri,duration:a.duration,size:a.size,tracks:a.periods[0].streams.map(function(a){return{id:a.id,active:!1,type:a.contentType,bandwidth:0,language:a.language,kind:a.kind||null,width:a.width,height:a.height,frameRate:a.frameRate,codecs:a.codecs}}),appMetadata:a.appMetadata}};function Ie(a,b,c){this.b={};this.i=c;this.m=a;this.l=b;this.j=this.a=null;this.f=this.g=this.h=this.c=0}Ie.prototype.o=function(){var a=this.j||Promise.resolve();this.b={};this.j=this.a=this.l=this.m=this.i=null;return a};function Je(a,b,c,d,e){a.b[b]=a.b[b]||[];a.b[b].push({uris:c.a(),M:c.M,D:c.D,mb:d,Ea:e})}
function Ke(a,b){a.c=0;a.h=0;a.g=0;a.f=0;F(a.b).forEach(function(a){a.forEach(function(a){null!=a.D?this.c+=a.D-a.M+1:this.g+=a.mb}.bind(this))}.bind(a));a.a=b;a.a.size=a.c;var c=F(a.b).map(function(a){var b=0,c=function(){if(!this.i)return Promise.reject(new v(9,9002));if(b>=a.length)return Promise.resolve();var d=a[b++];return Le(this,d).then(c)}.bind(this);return c()}.bind(a));a.b={};return a.j=Promise.all(c)}
function Le(a,b){var c=rc(b.uris,a.l);if(b.M||null!=b.D)c.headers.Range="bytes="+b.M+"-"+(null==b.D?"":b.D);var d;return a.m.request(1,c).then(function(a){if(!this.a)return Promise.reject(new v(9,9002));d=a.data.byteLength;return b.Ea(a.data)}.bind(a)).then(function(){if(!this.a)return Promise.reject(new v(9,9002));null==b.D?(this.a.size+=d,this.f+=b.mb):this.h+=d;var a=(this.h+this.f)/(this.c+this.g),c=He(this.a);this.i.progressCallback(c,a)}.bind(a))};function Me(){}Me.prototype.configure=function(){};Me.prototype.start=function(a){var b=/^offline:([0-9]+)$/.exec(a);if(!b)return Promise.reject(new v(1,9004,a));var c=Number(b[1]),d=new Ae;return d.init(Ge).then(function(){return d.get("manifest",c)}).then(function(a){if(!a)throw new v(9,9003,c);return Ne(a)}).then(function(a){return d.o().then(function(){return a})},function(a){return d.o().then(function(){throw a;})})};Me.prototype.stop=function(){return Promise.resolve()};
function Ne(a){var b=new Q(null,0);b.Aa(a.duration);var c=a.drmInfo?[a.drmInfo]:[];return{presentationTimeline:b,minBufferTime:10,offlineSessionIds:a.sessionIds,periods:a.periods.map(function(a){return{startTime:a.startTime,streamSets:a.streams.map(function(d){var e=d.segments.map(function(a,b){return new J(b,a.startTime,a.endTime,function(){return[a.uri]},0,null)});b.Ha(a.startTime,e);e=new O(e);return{language:d.language,type:d.contentType,primary:d.primary,drmInfos:c,streams:[{id:d.id,createSegmentIndex:Promise.resolve.bind(Promise),
findSegmentPosition:e.find.bind(e),getSegmentReference:e.get.bind(e),initSegmentReference:d.initSegmentUri?new yb(function(){return[d.initSegmentUri]},0,null):null,presentationTimeOffset:d.presentationTimeOffset,mimeType:d.mimeType,codecs:d.codecs,bandwidth:0,width:d.width||void 0,height:d.height||void 0,kind:d.kind,encrypted:d.encrypted,keyId:d.keyId,allowedByApplication:!0,allowedByKeySystem:!0}]}})}})}}sc["application/x-offline-manifest"]=Me;function Oe(a){if(/^offline:([0-9]+)$/.exec(a)){var b={uri:a,data:new ArrayBuffer(0),headers:{"content-type":"application/x-offline-manifest"}};return Promise.resolve(b)}if(b=/^offline:[0-9]+\/[0-9]+\/([0-9]+)$/.exec(a)){var c=Number(b[1]),d=new Ae;return d.init(Ge).then(function(){return d.get("segment",c)}).then(function(b){return d.o().then(function(){if(!b)throw new v(9,9003,c);return{uri:a,data:b.data,headers:{}}})})}return Promise.reject(new v(1,9004,a))}m("shaka.offline.OfflineScheme",Oe);
pc.offline=Oe;function Pe(){this.a=Promise.resolve();this.c=this.b=this.f=!1;this.g=new Promise(function(a){this.h=a}.bind(this))}Pe.prototype.then=function(a){this.a=this.a.then(a).then(function(a){return this.c?(this.h(),Promise.reject(this.i)):Promise.resolve(a)}.bind(this));return this};function Qe(a){a.f||(a.a=a.a.then(function(a){this.b=!0;return Promise.resolve(a)}.bind(a),function(a){this.b=!0;return Promise.reject(a)}.bind(a)));a.f=!0;return a.a}
Pe.prototype.cancel=function(a){if(this.b)return Promise.resolve();this.c=!0;this.i=a;return this.g};function Re(a,b,c,d,e){var f=e in d,g;for(g in b){var h=e+"."+g,l=f?d[e]:c[g],n=!!{".abr.manager":!0}[h];if(f||g in a)void 0===b[g]?void 0===l||f?delete a[g]:a[g]=l:n?a[g]=b[g]:"object"==typeof a[g]&&"object"==typeof b[g]?Re(a[g],b[g],l,d,h):typeof b[g]==typeof l&&(a[g]=b[g])}};function V(a,b){p.call(this);this.A=!1;this.f=a;this.m=null;this.v=new x;this.Ka=new q;this.pa=this.c=this.l=this.b=this.i=this.qa=this.C=this.F=this.g=this.h=null;this.Ra=1E9;this.oa=[];this.Da=!1;this.sa=!0;this.H=this.j=null;this.s={};this.a=Se(this);this.Ca={width:Infinity,height:Infinity};this.B=[];this.Y=this.K=this.ra=0;b&&b(this);this.h=new P(this.Uc.bind(this));this.qa=Te(this);for(var c=0;c<this.f.textTracks.length;++c){var d=this.f.textTracks[c];d.mode="disabled";"Shaka Player TextTrack"==
d.label&&(this.m=d)}this.m||(this.m=this.f.addTextTrack("subtitles","Shaka Player TextTrack"));this.m.mode="hidden";y(this.v,this.f,"error",this.tc.bind(this))}ba(V);m("shaka.Player",V);V.prototype.o=function(){this.A=!0;var a=Promise.resolve();this.j&&(a=this.j.cancel(new v(7,7E3)));return a.then(function(){var a=Promise.all([this.H,Ue(this),this.v?this.v.o():null,this.h?this.h.o():null]);this.a=this.h=this.Ka=this.v=this.m=this.f=null;return a}.bind(this))};V.prototype.destroy=V.prototype.o;
V.version="v2.0.8";var Ve={};V.registerSupportPlugin=function(a,b){Ve[a]=b};V.isBrowserSupported=function(){return!!window.Promise&&!!window.Uint8Array&&!!Array.prototype.forEach&&!!window.MediaSource&&!!window.MediaKeys&&!!window.navigator&&!!window.navigator.requestMediaKeySystemAccess&&!!window.MediaKeySystemAccess&&!!window.MediaKeySystemAccess.prototype.getConfiguration};
V.probeSupport=function(){return Xc().then(function(a){var b=uc(),c=cd();a={manifest:b,media:c,drm:a};for(var d in Ve)a[d]=Ve[d]();return a})};
V.prototype.load=function(a,b,c){var d=this.kb(),e=new Pe;this.j=e;this.dispatchEvent(new t("loading"));return Qe(e.then(function(){return d}).then(function(){return vc(a,this.h,this.a.manifest.retryParameters,c)}.bind(this)).then(function(b){this.l=new b;this.l.configure(this.a.manifest);return this.l.start(a,this.h,this.Na.bind(this),this.ia.bind(this),this.Rc.bind(this))}.bind(this)).then(function(b){if(0==b.periods.length)throw new v(4,4014);this.c=b;this.pa=a;this.g=new Jc(this.h,this.ia.bind(this),
this.Sc.bind(this));this.g.configure(this.a.drm);return this.g.init(b,!1)}.bind(this)).then(function(){this.c.periods.forEach(this.Na.bind(this));this.Y=Date.now()/1E3;return Promise.all([Mc(this.g,this.f),this.qa])}.bind(this)).then(function(){this.a.abr.manager.init(this.ib.bind(this));this.i=new Pd(this.f,this.c.presentationTimeline,1*Math.max(this.c.minBufferTime||0,this.a.streaming.rebufferingGoal),b||null,this.Nb.bind(this),this.Tc.bind(this));this.C=new bd(this.f,this.F,this.m);this.b=new Zd(this.i,
this.C,this.h,this.c,this.Qc.bind(this),this.Rb.bind(this),this.ia.bind(this));this.b.configure(this.a.streaming);return this.b.init()}.bind(this)).then(function(){this.c.periods.forEach(this.Na.bind(this));We(this);Xe(this);this.j=null}.bind(this)))["catch"](function(a){this.j==e&&(this.j=null,this.dispatchEvent(new t("unloading")));return Promise.reject(a)}.bind(this))};V.prototype.load=V.prototype.load;
function Te(a){a.F=new MediaSource;var b=new w;y(a.v,a.F,"sourceopen",b.resolve);a.f.src=window.URL.createObjectURL(a.F);return b}V.prototype.configure=function(a){a.abr&&a.abr.manager&&a.abr.manager!=this.a.abr.manager&&(this.a.abr.manager.stop(),a.abr.manager.init(this.ib.bind(this)));Re(this.a,a,Se(this),Ye(),"");Ze(this)};V.prototype.configure=V.prototype.configure;
function Ze(a){a.l&&a.l.configure(a.a.manifest);a.g&&a.g.configure(a.a.drm);if(a.b){a.b.configure(a.a.streaming);try{a.c.periods.forEach(a.Na.bind(a))}catch(b){a.ia(b)}$e(a,ce(a.b))}a.a.abr.enabled&&!a.sa?a.a.abr.manager.enable():a.a.abr.manager.disable();a.a.abr.manager.setDefaultEstimate(a.a.abr.defaultBandwidthEstimate)}V.prototype.getConfiguration=function(){var a=Se(this);Re(a,this.a,Se(this),Ye(),"");return a};V.prototype.getConfiguration=V.prototype.getConfiguration;
V.prototype.Jc=function(){var a=Se(this);a.abr&&a.abr.manager&&a.abr.manager!=this.a.abr.manager&&(this.a.abr.manager.stop(),a.abr.manager.init(this.ib.bind(this)));this.a=Se(this);Ze(this)};V.prototype.resetConfiguration=V.prototype.Jc;V.prototype.$b=function(){return this.f};V.prototype.getMediaElement=V.prototype.$b;V.prototype.rb=function(){return this.h};V.prototype.getNetworkingEngine=V.prototype.rb;V.prototype.Zb=function(){return this.pa};V.prototype.getManifestUri=V.prototype.Zb;
V.prototype.S=function(){return this.c?this.c.presentationTimeline.S():!1};V.prototype.isLive=V.prototype.S;V.prototype.fa=function(){return this.c?this.c.presentationTimeline.fa():!1};V.prototype.isInProgress=V.prototype.fa;V.prototype.Lc=function(){var a=0,b=0;this.c&&(b=this.c.presentationTimeline,a=b.ua(),b=b.Va());return{start:a,end:b}};V.prototype.seekRange=V.prototype.Lc;V.prototype.keySystem=function(){return this.g?this.g.keySystem():""};V.prototype.keySystem=V.prototype.keySystem;
V.prototype.drmInfo=function(){return this.g?this.g.b:null};V.prototype.drmInfo=V.prototype.drmInfo;V.prototype.dc=function(){return this.Da};V.prototype.isBuffering=V.prototype.dc;V.prototype.kb=function(){if(this.A)return Promise.resolve();this.dispatchEvent(new t("unloading"));var a=Promise.resolve();this.j&&(a=this.j.cancel(new v(7,7E3)));return a.then(function(){this.H||(this.H=af(this).then(function(){this.H=null}.bind(this)));return this.H}.bind(this))};V.prototype.unload=V.prototype.kb;
V.prototype.Ua=function(){return this.i?this.i.Ua():0};V.prototype.getPlaybackRate=V.prototype.Ua;V.prototype.ed=function(a){this.i&&Wd(this.i,a)};V.prototype.trickPlay=V.prototype.ed;V.prototype.Sb=function(){this.i&&Wd(this.i,1)};V.prototype.cancelTrickPlay=V.prototype.Sb;V.prototype.getTracks=function(){if(!this.b)return[];var a=de(this.b);return rd(ce(this.b),a).filter(function(a){return 0>this.oa.indexOf(a.id)}.bind(this))};V.prototype.getTracks=V.prototype.getTracks;
V.prototype.Mc=function(a,b){if(this.b){var c=sd(ce(this.b),a);if(c){var d=c.stream;d.allowedByApplication&&d.allowedByKeySystem&&(this.B.push({timestamp:Date.now()/1E3,id:d.id,type:a.type,fromAdaptation:!1}),c={},c[a.type]=d,"text"!=a.type&&(d=de(this.b).text,this.configure({abr:{enabled:!1}}),d&&(c.text=d)),bf(this,c,b))}}};V.prototype.selectTrack=V.prototype.Mc;V.prototype.gc=function(){return"showing"==this.m.mode};V.prototype.isTextTrackVisible=V.prototype.gc;
V.prototype.Oc=function(a){this.m.mode=a?"showing":"hidden";cf(this)};V.prototype.setTextTrackVisibility=V.prototype.Oc;
V.prototype.getStats=function(){df(this);var a={},b={},c=this.f&&this.f.getVideoPlaybackQuality?this.f.getVideoPlaybackQuality():{};this.b&&(b=de(this.b),a=b.video||{},b=b.audio||{});return{width:a.width||0,height:a.height||0,streamBandwidth:a.bandwidth+b.bandwidth||0,decodedFrames:Number(c.totalVideoFrames),droppedFrames:Number(c.droppedVideoFrames),estimatedBandwidth:this.a.abr.manager.getBandwidthEstimate(),playTime:this.ra,bufferingTime:this.K,switchHistory:this.B.slice(0)}};
V.prototype.getStats=V.prototype.getStats;
V.prototype.addTextTrack=function(a,b,c,d,e){if(!this.b)return Promise.reject();for(var f=ce(this.b),g,h=0;h<this.c.periods.length;h++)if(this.c.periods[h]==f){if(h==this.c.periods.length-1){if(g=this.c.presentationTimeline.ea()-f.startTime,Infinity==g)return Promise.reject()}else g=this.c.periods[h+1].startTime-f.startTime;break}var l={id:this.Ra++,createSegmentIndex:Promise.resolve.bind(Promise),findSegmentPosition:function(){return 1},getSegmentReference:function(b){return 1!=b?null:new J(1,0,
g,function(){return[a]},0,null)},initSegmentReference:null,presentationTimeOffset:0,mimeType:d,codecs:e||"",bandwidth:0,kind:c,encrypted:!1,keyId:null,language:b,allowedByApplication:!0,allowedByKeySystem:!0};d={language:b,type:"text",primary:!1,drmInfos:[],streams:[l]};this.oa.push(l.id);f.streamSets.push(d);return ee(this.b,l).then(function(){if(!this.A)return this.oa.splice(this.oa.indexOf(l.id),1),$e(this,f),We(this),{id:l.id,active:!1,type:"text",bandwidth:0,language:b,kind:c,width:null,height:null}}.bind(this))};
V.prototype.addTextTrack=V.prototype.addTextTrack;V.prototype.gb=function(a,b){this.Ca.width=a;this.Ca.height=b};V.prototype.setMaxHardwareResolution=V.prototype.gb;
function Ue(a){a.v&&a.v.la(a.F,"sourceopen");a.f&&(a.f.removeAttribute("src"),a.f.load());var b=Promise.all([a.a?a.a.abr.manager.stop():null,a.g?a.g.o():null,a.C?a.C.o():null,a.i?a.i.o():null,a.b?a.b.o():null,a.l?a.l.stop():null]);a.g=null;a.C=null;a.i=null;a.b=null;a.l=null;a.c=null;a.pa=null;a.qa=null;a.F=null;a.s={};a.B=[];a.ra=0;a.K=0;return b}function af(a){return a.l?Ue(a).then(function(){this.A||(this.Nb(!1),this.qa=Te(this))}.bind(a)):Promise.resolve()}
function Ye(){return{".drm.servers":"",".drm.clearKeys":"",".drm.advanced":{distinctiveIdentifierRequired:!1,persistentStateRequired:!1,videoRobustness:"",audioRobustness:"",serverCertificate:null}}}
function Se(a){return{drm:{retryParameters:qc(),servers:{},clearKeys:{},advanced:{}},manifest:{retryParameters:qc(),dash:{customScheme:function(a){if(a)return null},clockSyncUri:""}},streaming:{retryParameters:qc(),rebufferingGoal:2,bufferingGoal:10,bufferBehind:30,ignoreTextStreamFailures:!1,useRelativeCueTimestamps:!1},abr:{manager:a.Ka,enabled:!0,defaultBandwidthEstimate:5E5},preferredAudioLanguage:"",preferredTextLanguage:"",restrictions:{minWidth:0,maxWidth:Infinity,minHeight:0,maxHeight:Infinity,
minPixels:0,maxPixels:Infinity,minAudioBandwidth:0,maxAudioBandwidth:Infinity,minVideoBandwidth:0,maxVideoBandwidth:Infinity}}}k=V.prototype;k.Na=function(a){var b=this.b?de(this.b):{};pd(this.g,b,a);b=a.streamSets.some(td);od(a,this.a.restrictions,this.Ca)&&!this.j&&We(this);a=!a.streamSets.some(td);if(!b)throw new v(4,4011);if(a)throw new v(4,4012);};function bf(a,b,c){for(var d in b){var e=b[d],f=c||!1;"text"==d&&(f=!0);a.sa?a.s[d]={stream:e,Vb:f}:fe(a.b,d,e,f)}}
function df(a){if(a.c){var b=Date.now()/1E3;a.Da?a.K+=b-a.Y:a.ra+=b-a.Y;a.Y=b}}k.Uc=function(a,b,c){this.a.abr.manager.segmentDownloaded(a,b,c)};k.Nb=function(a){df(this);this.Da=a;this.dispatchEvent(new t("buffering",{buffering:a}))};k.Tc=function(){this.b&&ie(this.b)};
function ef(a,b,c){if(!F(b).some(td))return a.ia(new v(4,4012)),{};var d={};if(c)["video","audio","text"].forEach(function(a){a in b&&(d[a]=b[a])});else{c=de(a.b);for(var e in c){var f=c[e];f.allowedByApplication&&f.allowedByKeySystem&&b[e].language==f.language||(d[e]=b[e])}}if(Na(d))return{};ha(Object.keys(d));var g=a.a.abr.manager.chooseStreams(d);return Pa(d,function(a){return!!g[a]})?g:(a.ia(new v(4,4012)),{})}
function $e(a,b){var c={audio:!1,text:!1},d=ud(b,a.a,c),e=ef(a,d),f;for(f in e)a.B.push({timestamp:Date.now()/1E3,id:e[f].id,type:f,fromAdaptation:!0});bf(a,e,!0);Xe(a);d.text&&d.audio&&c.text&&d.text.language!=d.audio.language&&(a.m.mode="showing",cf(a))}k.Qc=function(a){this.sa=!0;this.a.abr.manager.disable();a=ud(a,this.a);a=ef(this,a,!0);for(var b in this.s)a[b]=this.s[b].stream;this.s={};for(b in a)this.B.push({timestamp:Date.now()/1E3,id:a[b].id,type:b,fromAdaptation:!0});this.j||We(this);return a};
k.Rb=function(){this.sa=!1;this.a.abr.enabled&&this.a.abr.manager.enable();for(var a in this.s){var b=this.s[a];fe(this.b,a,b.stream,b.Vb)}this.s={}};k.ib=function(a,b){var c=de(this.b),d;for(d in a){var e=a[d];c[d]!=e?this.B.push({timestamp:Date.now()/1E3,id:e.id,type:d,fromAdaptation:!0}):delete a[d]}if(!Na(a)&&this.b){for(d in a)fe(this.b,d,a[d],b||!1);Xe(this)}};function Xe(a){Promise.resolve().then(function(){this.A||this.dispatchEvent(new t("adaptation"))}.bind(a))}
function We(a){Promise.resolve().then(function(){this.A||this.dispatchEvent(new t("trackschanged"))}.bind(a))}function cf(a){a.dispatchEvent(new t("texttrackvisibility"))}k.ia=function(a){this.dispatchEvent(new t("error",{detail:a}))};k.Rc=function(a){this.dispatchEvent(a)};k.tc=function(){if(this.f.error){var a=this.f.error.code;if(1!=a){var b=this.f.error.msExtendedCode;b&&(0>b&&(b+=Math.pow(2,32)),b=b.toString(16));this.ia(new v(3,3016,a,b))}}};
k.Sc=function(a){var b=["output-restricted","internal-error"],c=ce(this.b),d=!1;c.streamSets.forEach(function(c){c.streams.forEach(function(c){var e=c.allowedByKeySystem;c.keyId&&c.keyId in a&&(c.allowedByKeySystem=0>b.indexOf(a[c.keyId]));e!=c.allowedByKeySystem&&(d=!0)})});$e(this,c);d&&We(this)};function W(a){this.a=new Ae;this.c=a;this.j=ff(this);this.g=null;this.v=!1;this.i=null;this.l=[];this.f=-1;this.m=0;this.b=null;this.h=new Ie(a.h,a.getConfiguration().streaming.retryParameters,this.j)}m("shaka.offline.Storage",W);function gf(){return!!window.indexedDB}W.support=gf;
W.prototype.o=function(){var a=this.l,b=this.a,c=this.h?this.h.o()["catch"](function(){}).then(function(){return Promise.all(a.map(function(a){return b.remove("segment",a)}))}).then(function(){return b.o()}):Promise.resolve();this.j=this.c=this.h=this.a=null;return c};W.prototype.destroy=W.prototype.o;W.prototype.configure=function(a){Re(this.j,a,ff(this),{},"")};W.prototype.configure=W.prototype.configure;
W.prototype.ad=function(a,b,c){function d(a){f=a}if(this.v)return Promise.reject(new v(9,9006));this.v=!0;var e,f=null;return hf(this).then(function(){Y(this);return jf(this,a,d,c)}.bind(this)).then(function(c){Y(this);this.b=c.manifest;this.g=c.Wb;if(this.b.presentationTimeline.S()||this.b.presentationTimeline.fa())throw new v(9,9005,a);this.b.periods.forEach(this.s.bind(this));this.f=this.a.b.manifest++;this.m=0;c=this.b.periods.map(this.A.bind(this));var d=this.g.b,f=Rc(this.g);if(d){if(!f.length)throw new v(9,
9007,a);d.initData=[]}e={key:this.f,originalManifestUri:a,duration:this.m,size:0,periods:c,sessionIds:f,drmInfo:d,appMetadata:b};return Ke(this.h,e)}.bind(this)).then(function(){Y(this);if(f)throw f;return Ee(this.a,"manifest",e)}.bind(this)).then(function(){return kf(this)}.bind(this)).then(function(){return He(e)}.bind(this))["catch"](function(a){return kf(this)["catch"](C).then(function(){throw a;})}.bind(this))};W.prototype.store=W.prototype.ad;
W.prototype.remove=function(a){function b(a){6013!=a.code&&(e=a)}var c=a.offlineUri,d=/^offline:([0-9]+)$/.exec(c);if(!d)return Promise.reject(new v(9,9004,c));var e=null,f,g,h=Number(d[1]);return hf(this).then(function(){Y(this);return this.a.get("manifest",h)}.bind(this)).then(function(a){Y(this);if(!a)throw new v(9,9003,c);f=a;a=Ne(f);g=new Jc(this.c.h,b,function(){});g.configure(this.c.getConfiguration().drm);return g.init(a,!0)}.bind(this)).then(function(){return Oc(g,f.sessionIds)}.bind(this)).then(function(){return g.o()}.bind(this)).then(function(){Y(this);
if(e)throw e;var b=f.periods.map(function(a){return a.streams.map(function(a){var b=a.segments.map(function(a){a=/^offline:[0-9]+\/[0-9]+\/([0-9]+)$/.exec(a.uri);return Number(a[1])});a.initSegmentUri&&(a=/^offline:[0-9]+\/[0-9]+\/([0-9]+)$/.exec(a.initSegmentUri),b.push(Number(a[1])));return b}).reduce(B,[])}).reduce(B,[]),c=0,d=b.length,g=this.j.progressCallback;return Fe(this.a,function(e){e=b.indexOf(e.key);0<=e&&(g(a,c/d),c++);return 0<=e}.bind(this))}.bind(this)).then(function(){Y(this);this.j.progressCallback(a,
1);return this.a.remove("manifest",h)}.bind(this))};W.prototype.remove=W.prototype.remove;W.prototype.list=function(){var a=[];return hf(this).then(function(){Y(this);return this.a.forEach("manifest",function(b){a.push(He(b))})}.bind(this)).then(function(){return a})};W.prototype.list=W.prototype.list;
function jf(a,b,c,d){function e(){}var f=a.c.h,g=a.c.getConfiguration(),h,l,n;return vc(b,f,g.manifest.retryParameters,d).then(function(a){Y(this);n=new a;n.configure(g.manifest);return n.start(b,f,this.s.bind(this),c)}.bind(a)).then(function(a){Y(this);h=a;l=new Jc(f,c,e);l.configure(g.drm);return l.init(h,!0)}.bind(a)).then(function(){Y(this);return lf(h)}.bind(a)).then(function(){Y(this);return Nc(l)}.bind(a)).then(function(){Y(this);return n.stop()}.bind(a)).then(function(){Y(this);return{manifest:h,
Wb:l}}.bind(a))["catch"](function(a){if(n)return n.stop().then(function(){throw a;});throw a;})}
W.prototype.B=function(a){var b=[],c=a.filter(function(a){return"video"==a.type&&480>=a.height});c.sort(function(a,b){return b.bandwidth-a.bandwidth});c.length&&b.push(c[0]);for(var d=Ec(this.c.getConfiguration().preferredAudioLanguage),c=[0,Cc,Dc],e=a.filter(function(a){return"audio"==a.type}),c=c.map(function(a){return e.filter(function(b){b=Ec(b.language);return Bc(a,d,b)})}),f=e,g=0;g<c.length;g++)c[g].length&&(f=c[g]);f.sort(function(a,b){return a.bandwidth-b.bandwidth});f.length&&b.push(f[Math.floor(f.length/
2)]);var c=Ec(this.c.getConfiguration().preferredTextLanguage),h=Bc.bind(null,Dc,c);b.push.apply(b,a.filter(function(a){var b=Ec(a.language);return"text"==a.type&&h(b)}));return b};function ff(a){return{trackSelectionCallback:a.B.bind(a),progressCallback:function(a,c){if(a||c)return null}}}function hf(a){return a.a.a?Promise.resolve():a.a.init(Ge)}
W.prototype.s=function(a){function b(a,b,c){b=b.filter(function(a){return a.type==c});return 0==b.length?null:sd(a,b[0]).stream}var c={};this.i&&(c={video:b(this.b.periods[0],this.i,"video"),audio:b(this.b.periods[0],this.i,"audio")});pd(this.g,c,a);od(a,this.c.getConfiguration().restrictions,{width:Infinity,height:Infinity})};function kf(a){var b=a.g?a.g.o():Promise.resolve();a.g=null;a.b=null;a.v=!1;a.i=null;a.l=[];a.f=-1;return b}
function lf(a){a=a.periods.map(function(a){return a.streamSets}).reduce(B,[]).map(function(a){return a.streams}).reduce(B,[]);return Promise.all(a.map(function(a){return a.createSegmentIndex()}))}
W.prototype.A=function(a){var b=rd(a,null),b=this.j.trackSelectionCallback(b);this.i||(this.i=b,this.b.periods.forEach(this.s.bind(this)));for(var c=b.length-1;0<c;--c){for(var d=!1,e=c-1;0<=e;--e)if(b[c].type==b[e].type&&b[c].kind==b[e].kind&&b[c].language==b[e].language){d=!0;break}if(d)break}b=b.map(function(b){b=sd(a,b);return mf(this,a,b.bd,b.stream)}.bind(this));return{startTime:a.startTime,streams:b}};
function mf(a,b,c,d){for(var e=[],f=a.b.presentationTimeline.ta(),g=f,h=d.findSegmentPosition(f),l=null!=h?d.getSegmentReference(h):null;l;){var n=a.a.b.segment++;Je(a.h,c.type,l,(l.endTime-l.startTime)*d.bandwidth/8,function(a,b,c,d){b={key:a,data:d,manifestKey:this.f,streamNumber:c,segmentNumber:b};this.l.push(a);return Ee(this.a,"segment",b)}.bind(a,n,l.position,d.id));e.push({startTime:l.startTime,endTime:l.endTime,uri:"offline:"+a.f+"/"+d.id+"/"+n});g=l.endTime+b.startTime;l=d.getSegmentReference(++h)}a.m=
Math.max(a.m,g-f);b=null;d.initSegmentReference&&(n=a.a.b.segment++,b="offline:"+a.f+"/"+d.id+"/"+n,Je(a.h,c.type,d.initSegmentReference,0,function(a,b){var c={key:n,data:b,manifestKey:this.f,streamNumber:a,segmentNumber:-1};this.l.push(n);return Ee(this.a,"segment",c)}.bind(a,d.id)));return{id:d.id,primary:c.primary,presentationTimeOffset:d.presentationTimeOffset||0,contentType:c.type,mimeType:d.mimeType,codecs:d.codecs,frameRate:d.frameRate,kind:d.kind,language:c.language,width:d.width||null,height:d.height||
null,initSegmentUri:b,encrypted:d.encrypted,keyId:d.keyId,segments:e}}function Y(a){if(!a.c)throw new v(9,9002);}Ve.offline=gf;m("shaka.polyfill.installAll",function(){for(var a=0;a<nf.length;++a)nf[a]()});var nf=[];function of(a){nf.push(a)}m("shaka.polyfill.register",of);function pf(a){var b=a.type.replace(/^(webkit|moz|MS)/,"").toLowerCase(),b=new Event(b,a);a.target.dispatchEvent(b)}
of(function(){if(window.Document){var a=Element.prototype;a.requestFullscreen=a.requestFullscreen||a.mozRequestFullScreen||a.msRequestFullscreen||a.webkitRequestFullscreen;a=Document.prototype;a.exitFullscreen=a.exitFullscreen||a.mozCancelFullScreen||a.msExitFullscreen||a.webkitExitFullscreen;"fullscreenElement"in document||(Object.defineProperty(document,"fullscreenElement",{get:function(){return document.mozFullScreenElement||document.msFullscreenElement||document.webkitFullscreenElement}}),Object.defineProperty(document,
"fullscreenEnabled",{get:function(){return document.mozFullScreenEnabled||document.msFullscreenEnabled||document.webkitFullscreenEnabled}}));document.addEventListener("webkitfullscreenchange",pf);document.addEventListener("webkitfullscreenerror",pf);document.addEventListener("mozfullscreenchange",pf);document.addEventListener("mozfullscreenerror",pf);document.addEventListener("MSFullscreenChange",pf);document.addEventListener("MSFullscreenError",pf)}});of(function(){var a=navigator.userAgent;a&&0<=a.indexOf("CrKey")&&delete window.indexedDB});function qf(a){this.c=[];this.b=[];this.a=[];for(a=new Jb(new DataView(a.buffer));Lb(a);){var b=Sb(1886614376,a);if(-1==b)break;var c=a.a-8,d=Mb(a);if(1<d)N(a,b-(a.a-c));else{N(a,3);var e=Ta(Qb(a,16)),f=[];if(0<d)for(var d=M(a),g=0;g<d;++g){var h=Ta(Qb(a,16));f.push(h)}d=M(a);N(a,d);this.b.push.apply(this.b,f);this.c.push(e);this.a.push({start:c,end:a.a-1});a.a!=c+b&&N(a,b-(a.a-c))}}};function rf(a,b){try{var c=new sf(a,b);return Promise.resolve(c)}catch(d){return Promise.reject(d)}}
function sf(a,b){this.keySystem=a;for(var c=!1,d=0;d<b.length;++d){var e=b[d],f={audioCapabilities:[],videoCapabilities:[],persistentState:"optional",distinctiveIdentifier:"optional",initDataTypes:e.initDataTypes,sessionTypes:["temporary"],label:e.label},g=!1;if(e.audioCapabilities)for(var h=0;h<e.audioCapabilities.length;++h){var l=e.audioCapabilities[h];if(l.contentType){var g=!0,n=l.contentType.split(";")[0];MSMediaKeys.isTypeSupported(this.keySystem,n)&&(f.audioCapabilities.push(l),c=!0)}}if(e.videoCapabilities)for(h=
0;h<e.videoCapabilities.length;++h)l=e.videoCapabilities[h],l.contentType&&(g=!0,n=l.contentType.split(";")[0],MSMediaKeys.isTypeSupported(this.keySystem,n)&&(f.videoCapabilities.push(l),c=!0));g||(c=MSMediaKeys.isTypeSupported(this.keySystem,"video/mp4"));"required"==e.persistentState&&(f.persistentState="required",f.sessionTypes=["persistent-license"]);if(c){this.a=f;return}}c=Error("Unsupported keySystem");c.name="NotSupportedError";c.code=DOMException.NOT_SUPPORTED_ERR;throw c;}
sf.prototype.createMediaKeys=function(){var a=new tf(this.keySystem);return Promise.resolve(a)};sf.prototype.getConfiguration=function(){return this.a};function uf(a){var b=this.mediaKeys;b&&b!=a&&vf(b,null);delete this.mediaKeys;return(this.mediaKeys=a)?vf(a,this):Promise.resolve()}function tf(a){this.a=new MSMediaKeys(a);this.b=new x}tf.prototype.createSession=function(a){if("temporary"!=(a||"temporary"))throw new TypeError("Session type "+a+" is unsupported on this platform.");return new wf(this.a)};
tf.prototype.setServerCertificate=function(){return Promise.reject(Error("setServerCertificate not supported on this platform."))};function vf(a,b){function c(){b.msSetMediaKeys(d.a);b.removeEventListener("loadedmetadata",c)}Ca(a.b);if(!b)return Promise.resolve();y(a.b,b,"msneedkey",xf);var d=a;try{return 1<=b.readyState?b.msSetMediaKeys(a.a):b.addEventListener("loadedmetadata",c),Promise.resolve()}catch(e){return Promise.reject(e)}}
function wf(a){p.call(this);this.c=null;this.g=a;this.b=this.a=null;this.f=new x;this.sessionId="";this.expiration=NaN;this.closed=new w;this.keyStatuses=new yf}ba(wf);k=wf.prototype;k.generateRequest=function(a,b){this.a=new w;try{this.c=this.g.createSession("video/mp4",new Uint8Array(b),null),y(this.f,this.c,"mskeymessage",this.pc.bind(this)),y(this.f,this.c,"mskeyadded",this.nc.bind(this)),y(this.f,this.c,"mskeyerror",this.oc.bind(this)),zf(this,"status-pending")}catch(c){this.a.reject(c)}return this.a};
k.load=function(){return Promise.reject(Error("MediaKeySession.load not yet supported"))};k.update=function(a){this.b=new w;try{this.c.update(new Uint8Array(a))}catch(b){this.b.reject(b)}return this.b};k.close=function(){try{this.c.close(),this.closed.resolve(),Ca(this.f)}catch(a){this.closed.reject(a)}return this.closed};k.remove=function(){return Promise.reject(Error("MediaKeySession.remove is only applicable for persistent licenses, which are not supported on this platform"))};
function xf(a){var b=document.createEvent("CustomEvent");b.initCustomEvent("encrypted",!1,!1,null);b.initDataType="cenc";if(a=a.initData){var c=new qf(a);if(!(1>=c.a.length)){for(var d=[],e=0;e<c.a.length;e++)d.push(a.subarray(c.a[e].start,c.a[e].end+1));e=Af;a=[];for(c=0;c<d.length;++c){for(var f=!1,g=0;g<a.length&&!(f=e?e(d[c],a[g]):d[c]===a[g]);++g);f||a.push(d[c])}for(e=d=0;e<a.length;e++)d+=a[e].length;d=new Uint8Array(d);for(e=c=0;e<a.length;e++)d.set(a[e],c),c+=a[e].length;a=d}}b.initData=
a;this.dispatchEvent(b)}function Af(a,b){return Ua(a,b)}k.pc=function(a){this.a&&(this.a.resolve(),this.a=null);this.dispatchEvent(new t("message",{messageType:void 0==this.keyStatuses.Wa()?"licenserequest":"licenserenewal",message:a.message.buffer}))};k.nc=function(){this.a?(zf(this,"usable"),this.a.resolve(),this.a=null):this.b&&(zf(this,"usable"),this.b.resolve(),this.b=null)};
k.oc=function(){var a=Error("EME PatchedMediaKeysMs key error");a.errorCode=this.c.error;if(this.a)this.a.reject(a),this.a=null;else if(this.b)this.b.reject(a),this.b=null;else switch(this.c.error.code){case MSMediaKeyError.MS_MEDIA_KEYERR_OUTPUT:case MSMediaKeyError.MS_MEDIA_KEYERR_HARDWARECHANGE:zf(this,"output-not-allowed");default:zf(this,"internal-error")}};function zf(a,b){a.keyStatuses.hb(b);a.dispatchEvent(new t("keystatuseschange"))}function yf(){this.size=0;this.a=void 0}var Bf;k=yf.prototype;
k.hb=function(a){this.size=void 0==a?0:1;this.a=a};k.Wa=function(){return this.a};k.forEach=function(a){this.a&&a(this.a,Bf)};k.get=function(a){if(this.has(a))return this.a};k.has=function(a){var b=Bf;return this.a&&Ua(new Uint8Array(a),new Uint8Array(b))?!0:!1};k.keys=function(){};function Cf(){return Promise.reject(Error("The key system specified is not supported."))}function Df(a){return a?Promise.reject(Error("MediaKeys not supported.")):Promise.resolve()}function Ef(){throw new TypeError("Illegal constructor.");}Ef.prototype.createSession=function(){};Ef.prototype.setServerCertificate=function(){};function Ff(){throw new TypeError("Illegal constructor.");}Ff.prototype.getConfiguration=function(){};Ff.prototype.createMediaKeys=function(){};var Gf="";function Hf(a){Gf=a;If=(new Uint8Array([0])).buffer;navigator.requestMediaKeySystemAccess=Jf;delete HTMLMediaElement.prototype.mediaKeys;HTMLMediaElement.prototype.mediaKeys=null;HTMLMediaElement.prototype.setMediaKeys=Kf;window.MediaKeys=Lf;window.MediaKeySystemAccess=Mf}function Nf(a){var b=Gf;return b?b+a.charAt(0).toUpperCase()+a.slice(1):a}function Jf(a,b){try{var c=new Mf(a,b);return Promise.resolve(c)}catch(d){return Promise.reject(d)}}
function Kf(a){var b=this.mediaKeys;b&&b!=a&&Of(b,null);delete this.mediaKeys;(this.mediaKeys=a)&&Of(a,this);return Promise.resolve()}
function Mf(a,b){this.a=this.keySystem=a;var c=!0;"org.w3.clearkey"==a&&(this.a="webkit-org.w3.clearkey",c=!1);var d=!1,e;e=document.getElementsByTagName("video");e=e.length?e[0]:document.createElement("video");for(var f=0;f<b.length;++f){var g=b[f],h={audioCapabilities:[],videoCapabilities:[],persistentState:"optional",distinctiveIdentifier:"optional",initDataTypes:g.initDataTypes,sessionTypes:["temporary"],label:g.label},l=!1;if(g.audioCapabilities)for(var n=0;n<g.audioCapabilities.length;++n){var r=
g.audioCapabilities[n];if(r.contentType){var l=!0,u=r.contentType.split(";")[0];e.canPlayType(u,this.a)&&(h.audioCapabilities.push(r),d=!0)}}if(g.videoCapabilities)for(n=0;n<g.videoCapabilities.length;++n)r=g.videoCapabilities[n],r.contentType&&(l=!0,e.canPlayType(r.contentType,this.a)&&(h.videoCapabilities.push(r),d=!0));l||(d=e.canPlayType("video/mp4",this.a)||e.canPlayType("video/webm",this.a));"required"==g.persistentState&&(c?(h.persistentState="required",h.sessionTypes=["persistent-license"]):
d=!1);if(d){this.b=h;return}}c="Unsupported keySystem";if("org.w3.clearkey"==a||"com.widevine.alpha"==a)c="None of the requested configurations were supported.";c=Error(c);c.name="NotSupportedError";c.code=DOMException.NOT_SUPPORTED_ERR;throw c;}Mf.prototype.createMediaKeys=function(){var a=new Lf(this.a);return Promise.resolve(a)};Mf.prototype.getConfiguration=function(){return this.b};function Lf(a){this.g=a;this.b=null;this.a=new x;this.c=[];this.f={}}
function Of(a,b){a.b=b;Ca(a.a);var c=Gf;b&&(y(a.a,b,c+"needkey",a.yc.bind(a)),y(a.a,b,c+"keymessage",a.xc.bind(a)),y(a.a,b,c+"keyadded",a.vc.bind(a)),y(a.a,b,c+"keyerror",a.wc.bind(a)))}k=Lf.prototype;k.createSession=function(a){var b=a||"temporary";if("temporary"!=b&&"persistent-license"!=b)throw new TypeError("Session type "+a+" is unsupported on this platform.");a=this.b||document.createElement("video");a.src||(a.src="about:blank");b=new Pf(a,this.g,b);this.c.push(b);return b};
k.setServerCertificate=function(){return Promise.reject(Error("setServerCertificate not supported on this platform."))};k.yc=function(a){var b=document.createEvent("CustomEvent");b.initCustomEvent("encrypted",!1,!1,null);b.initDataType="webm";b.initData=a.initData;this.b.dispatchEvent(b)};k.xc=function(a){var b=Qf(this,a.sessionId);b&&(a=new t("message",{messageType:void 0==b.keyStatuses.Wa()?"licenserequest":"licenserenewal",message:a.message}),b.b&&(b.b.resolve(),b.b=null),b.dispatchEvent(a))};
k.vc=function(a){if(a=Qf(this,a.sessionId))Rf(a,"usable"),a.a&&a.a.resolve(),a.a=null};
k.wc=function(a){var b=Qf(this,a.sessionId);if(b){var c=Error("EME v0.1b key error");c.errorCode=a.errorCode;c.errorCode.systemCode=a.systemCode;!a.sessionId&&b.b?(c.method="generateRequest",45==a.systemCode&&(c.message="Unsupported session type."),b.b.reject(c),b.b=null):a.sessionId&&b.a?(c.method="update",b.a.reject(c),b.a=null):(c=a.systemCode,a.errorCode.code==MediaKeyError.MEDIA_KEYERR_OUTPUT?Rf(b,"output-restricted"):1==c?Rf(b,"expired"):Rf(b,"internal-error"))}};
function Qf(a,b){var c=a.f[b];return c?c:(c=a.c.shift())?(c.sessionId=b,a.f[b]=c):null}function Pf(a,b,c){p.call(this);this.f=a;this.h=!1;this.a=this.b=null;this.c=b;this.g=c;this.sessionId="";this.expiration=NaN;this.closed=new w;this.keyStatuses=new Sf}ba(Pf);
function Tf(a,b,c){if(a.h)return Promise.reject(Error("The session is already initialized."));a.h=!0;var d;try{if("persistent-license"==a.g)if(c)d=new Uint8Array(Ib("LOAD_SESSION|"+c));else{var e=Ib("PERSISTENT|"),f=new Uint8Array(e.byteLength+b.byteLength);f.set(new Uint8Array(e),0);f.set(new Uint8Array(b),e.byteLength);d=f}else d=new Uint8Array(b)}catch(h){return Promise.reject(h)}a.b=new w;var g=Nf("generateKeyRequest");try{a.f[g](a.c,d)}catch(h){if("InvalidStateError"!=h.name)return a.b=null,
Promise.reject(h);setTimeout(function(){try{this.f[g](this.c,d)}catch(l){this.b.reject(l),this.b=null}}.bind(a),10)}return a.b}k=Pf.prototype;
k.jb=function(a,b){if(this.a)this.a.then(this.jb.bind(this,a,b))["catch"](this.jb.bind(this,a,b));else{this.a=a;var c,d;"webkit-org.w3.clearkey"==this.c?(c=Eb(b),d=JSON.parse(c),"oct"!=d.keys[0].kty&&(this.a.reject(Error("Response is not a valid JSON Web Key Set.")),this.a=null),c=Ra(d.keys[0].k),d=Ra(d.keys[0].kid)):(c=new Uint8Array(b),d=null);var e=Nf("addKey");try{this.f[e](this.c,c,d,this.sessionId)}catch(f){this.a.reject(f),this.a=null}}};
function Rf(a,b){a.keyStatuses.hb(b);a.dispatchEvent(new t("keystatuseschange"))}k.generateRequest=function(a,b){return Tf(this,b,null)};k.load=function(a){return"persistent-license"==this.g?Tf(this,null,a):Promise.reject(Error("Not a persistent session."))};k.update=function(a){var b=new w;this.jb(b,a);return b};
k.close=function(){if("persistent-license"!=this.g){if(!this.sessionId)return this.closed.reject(Error("The session is not callable.")),this.closed;var a=Nf("cancelKeyRequest");try{this.f[a](this.c,this.sessionId)}catch(b){}}this.closed.resolve();return this.closed};k.remove=function(){return"persistent-license"!=this.g?Promise.reject(Error("Not a persistent session.")):this.close()};function Sf(){this.size=0;this.a=void 0}var If;k=Sf.prototype;k.hb=function(a){this.size=void 0==a?0:1;this.a=a};
k.Wa=function(){return this.a};k.forEach=function(a){this.a&&a(this.a,If)};k.get=function(a){if(this.has(a))return this.a};k.has=function(a){var b=If;return this.a&&Ua(new Uint8Array(a),new Uint8Array(b))?!0:!1};k.keys=function(){};of(function(){!window.HTMLVideoElement||navigator.requestMediaKeySystemAccess&&MediaKeySystemAccess.prototype.getConfiguration||(HTMLMediaElement.prototype.webkitGenerateKeyRequest?Hf("webkit"):HTMLMediaElement.prototype.generateKeyRequest?Hf(""):window.MSMediaKeys?(Bf=(new Uint8Array([0])).buffer,delete HTMLMediaElement.prototype.mediaKeys,HTMLMediaElement.prototype.mediaKeys=null,HTMLMediaElement.prototype.setMediaKeys=uf,window.MediaKeys=tf,window.MediaKeySystemAccess=sf,navigator.requestMediaKeySystemAccess=
rf):(navigator.requestMediaKeySystemAccess=Cf,delete HTMLMediaElement.prototype.mediaKeys,HTMLMediaElement.prototype.mediaKeys=null,HTMLMediaElement.prototype.setMediaKeys=Df,window.MediaKeys=Ef,window.MediaKeySystemAccess=Ff))});function Uf(){var a=MediaSource.prototype.addSourceBuffer;MediaSource.prototype.addSourceBuffer=function(){var b=a.apply(this,arguments);b.abort=function(){};return b}}
function Vf(){var a=MediaSource.prototype.endOfStream;MediaSource.prototype.endOfStream=function(){for(var b=0,d=0;d<this.sourceBuffers.length;++d)var e=this.sourceBuffers[d],e=e.buffered.end(e.buffered.length-1),b=Math.max(b,e);if(!isNaN(this.duration)&&b<this.duration)for(this.sb=!0,d=0;d<this.sourceBuffers.length;++d)e=this.sourceBuffers[d],e.ob=!1;return a.apply(this,arguments)};var b=MediaSource.prototype.addSourceBuffer;MediaSource.prototype.addSourceBuffer=function(){var a=b.apply(this,arguments);
a.F=this;a.addEventListener("updateend",Wf,!1);this.a||(this.addEventListener("sourceclose",Xf,!1),this.a=!0);return a}}function Wf(a){var b=a.target,c=b.F;if(c.sb){a.preventDefault();a.stopPropagation();a.stopImmediatePropagation();b.ob=!0;for(a=0;a<c.sourceBuffers.length;++a)if(0==c.sourceBuffers[a].ob)return;c.sb=!1}}function Xf(a){a=a.target;for(var b=0;b<a.sourceBuffers.length;++b)a.sourceBuffers[b].removeEventListener("updateend",Wf,!1);a.removeEventListener("sourceclose",Xf,!1)}
of(function(){if(window.MediaSource){var a=navigator.vendor,b=navigator.appVersion;!a||!b||0>a.indexOf("Apple")||(0<=b.indexOf("Version/8")?window.MediaSource=null:0<=b.indexOf("Version/9")?Uf():0<=b.indexOf("Version/10")&&(Uf(),Vf()))}});function Z(a){this.c=[];this.b=[];this.ka=Yf;if(a)try{a(this.X.bind(this),this.a.bind(this))}catch(b){this.a(b)}}var Yf=0;function Zf(a){var b=new Z;b.X(void 0);return b.then(function(){return a})}function $f(a){var b=new Z;b.a(a);return b}function ag(a){function b(a,b,c){a.ka==Yf&&(e[b]=c,d++,d==e.length&&a.X(e))}var c=new Z;if(!a.length)return c.X([]),c;for(var d=0,e=Array(a.length),f=c.a.bind(c),g=0;g<a.length;++g)a[g]&&a[g].then?a[g].then(b.bind(null,c,g),f):b(c,g,a[g]);return c}
function bg(a){for(var b=new Z,c=b.X.bind(b),d=b.a.bind(b),e=0;e<a.length;++e)a[e]&&a[e].then?a[e].then(c,d):c(a[e]);return b}Z.prototype.then=function(a,b){var c=new Z;switch(this.ka){case 1:cg(this,c,a);break;case 2:cg(this,c,b);break;case Yf:this.c.push({J:c,Ea:a}),this.b.push({J:c,Ea:b})}return c};Z.prototype["catch"]=function(a){return this.then(void 0,a)};
Z.prototype.X=function(a){if(this.ka==Yf){this.Pa=a;this.ka=1;for(a=0;a<this.c.length;++a)cg(this,this.c[a].J,this.c[a].Ea);this.c=[];this.b=[]}};Z.prototype.a=function(a){if(this.ka==Yf){this.Pa=a;this.ka=2;for(a=0;a<this.b.length;++a)cg(this,this.b[a].J,this.b[a].Ea);this.c=[];this.b=[]}};
function cg(a,b,c){dg.push(function(){if(c&&"function"==typeof c){try{var a=c(this.Pa)}catch(f){b.a(f);return}var e;try{e=a&&a.then}catch(f){b.a(f);return}a instanceof Z?a==b?b.a(new TypeError("Chaining cycle detected")):a.then(b.X.bind(b),b.a.bind(b)):e?eg(a,e,b):b.X(a)}else 1==this.ka?b.X(this.Pa):b.a(this.Pa)}.bind(a));null==fg&&(fg=gg(hg))}
function eg(a,b,c){try{var d=!1;b.call(a,function(a){if(!d){d=!0;var b;try{b=a&&a.then}catch(g){c.a(g);return}b?eg(a,b,c):c.X(a)}},c.a.bind(c))}catch(e){c.a(e)}}function hg(){for(;dg.length;){null!=fg&&(ig(fg),fg=null);var a=dg;dg=[];for(var b=0;b<a.length;++b)a[b]()}}function gg(){return 0}function ig(){}var fg=null,dg=[];
of(function(a){window.setImmediate?(gg=function(a){return window.setImmediate(a)},ig=function(a){return window.clearImmediate(a)}):(gg=function(a){return window.setTimeout(a,0)},ig=function(a){return window.clearTimeout(a)});if(!window.Promise||a)window.Promise=Z,window.Promise.resolve=Zf,window.Promise.reject=$f,window.Promise.all=ag,window.Promise.race=bg,window.Promise.prototype.then=Z.prototype.then,window.Promise.prototype["catch"]=Z.prototype["catch"]});function jg(){return{droppedVideoFrames:this.webkitDroppedFrameCount,totalVideoFrames:this.webkitDecodedFrameCount,corruptedVideoFrames:0,creationTime:NaN,totalFrameDelay:0}}of(function(){if(window.HTMLVideoElement){var a=HTMLVideoElement.prototype;!a.getVideoPlaybackQuality&&"webkitDroppedFrameCount"in a&&(a.getVideoPlaybackQuality=jg)}});function kg(a,b,c){return new window.TextTrackCue(a,b,c)}function lg(a,b,c){return new window.TextTrackCue(a+"-"+b+"-"+c,a,b,c)}of(function(){if(!window.VTTCue&&window.TextTrackCue){var a=TextTrackCue.length;if(3==a)window.VTTCue=kg;else if(6==a)window.VTTCue=lg;else{var b;try{b=!!kg(1,2,"")}catch(c){b=!1}b&&(window.VTTCue=kg)}}});}.call(g,this));
Eif (typeof(module)!="undefined"&&module.exports)module.exports=g.shaka;
else if (typeof(define)!="undefined" && define.amd)define(function(){return g.shaka});
else this.shaka=g.shaka;
})();
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| chromecast.js | 60.78% | (31 / 51) | 100% | (0 / 0) | 0% | (0 / 29) | 60.78% | (31 / 51) | |
| fullscreen.js | 50% | (1 / 2) | 100% | (0 / 0) | 0% | (0 / 2) | 50% | (1 / 2) | |
| jwk_set.js | 33.33% | (2 / 6) | 100% | (0 / 0) | 0% | (0 / 2) | 33.33% | (2 / 6) | |
| mediakeys.js | 27.5% | (11 / 40) | 100% | (0 / 0) | 0% | (0 / 23) | 27.5% | (11 / 40) | |
| mediasession.js | 100% | (3 / 3) | 100% | (0 / 0) | 0% | (0 / 1) | 100% | (3 / 3) | |
| msmediakeys.js | 57.14% | (12 / 21) | 100% | (0 / 0) | 0% | (0 / 11) | 57.14% | (12 / 21) | |
| prefixed_eme.js | 18.75% | (3 / 16) | 100% | (0 / 0) | 0% | (0 / 6) | 18.75% | (3 / 16) | |
| texttrack.js | 16.67% | (1 / 6) | 100% | (0 / 0) | 100% | (0 / 0) | 16.67% | (1 / 6) | |
| xmlhttprequest.js | 20% | (1 / 5) | 100% | (0 / 0) | 100% | (0 / 0) | 20% | (1 / 5) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Google Cast API externs.
* Based on the {@link https://goo.gl/psEjEh Google Cast API}.
* @externs
*/
/** @type {function(boolean)} */
var __onGCastApiAvailable;
/** @const */
var cast = {};
/** @const */
cast.receiver = {};
/** @const */
cast.receiver.system = {};
/**
* @constructor
* @struct
*/
cast.receiver.system.SystemVolumeData = function() {};
/** @type {number} */
cast.receiver.system.SystemVolumeData.prototype.level;
/** @type {boolean} */
cast.receiver.system.SystemVolumeData.prototype.muted;
/**
* @constructor
* @struct
*/
cast.receiver.CastMessageBus = function() {};
/** @param {*} message */
cast.receiver.CastMessageBus.prototype.broadcast = function(message) {};
/**
* @param {string} senderId
* @return {!cast.receiver.CastChannel}
*/
cast.receiver.CastMessageBus.prototype.getCastChannel = function(senderId) {};
/** @type {Function} */
cast.receiver.CastMessageBus.prototype.onMessage;
/**
* @constructor
* @struct
*/
cast.receiver.CastMessageBus.Event = function() {};
/** @type {?} */
cast.receiver.CastMessageBus.Event.prototype.data;
/** @type {string} */
cast.receiver.CastMessageBus.Event.prototype.senderId;
/**
* @constructor
* @struct
*/
cast.receiver.CastChannel = function() {};
/** @param {*} message */
cast.receiver.CastChannel.prototype.send = function(message) {};
/**
* @constructor
* @struct
*/
cast.receiver.CastReceiverManager = function() {};
/** @return {cast.receiver.CastReceiverManager} */
cast.receiver.CastReceiverManager.getInstance = function() {};
/**
* @param {string} namespace
* @param {string=} opt_messageType
* @return {cast.receiver.CastMessageBus}
*/
cast.receiver.CastReceiverManager.prototype.getCastMessageBus = function(
namespace, opt_messageType) {};
/** @return {Array.<string>} */
cast.receiver.CastReceiverManager.prototype.getSenders = function() {};
cast.receiver.CastReceiverManager.prototype.start = function() {};
cast.receiver.CastReceiverManager.prototype.stop = function() {};
/** @return {?cast.receiver.system.SystemVolumeData} */
cast.receiver.CastReceiverManager.prototype.getSystemVolume = function() {};
/** @param {number} level */
cast.receiver.CastReceiverManager.prototype.setSystemVolumeLevel =
function(level) {};
/** @param {number} muted */
cast.receiver.CastReceiverManager.prototype.setSystemVolumeMuted =
function(muted) {};
/** @return {boolean} */
cast.receiver.CastReceiverManager.prototype.isSystemReady = function() {};
/** @type {Function} */
cast.receiver.CastReceiverManager.prototype.onSenderConnected;
/** @type {Function} */
cast.receiver.CastReceiverManager.prototype.onSenderDisconnected;
/** @type {Function} */
cast.receiver.CastReceiverManager.prototype.onSystemVolumeChanged;
/** @const */
cast.__platform__;
/**
* @param {string} type
* @return {boolean}
*/
cast.__platform__.canDisplayType = function(type) {};
/** @const */
chrome.cast = {};
/** @type {boolean} */
chrome.cast.isAvailable;
/**
* @param {chrome.cast.ApiConfig} apiConfig
* @param {Function} successCallback
* @param {Function} errorCallback
*/
chrome.cast.initialize = function(apiConfig, successCallback, errorCallback) {};
/**
* @param {Function} successCallback
* @param {Function} errorCallback
* @param {chrome.cast.SessionRequest=} opt_sessionRequest
*/
chrome.cast.requestSession = function(
successCallback, errorCallback, opt_sessionRequest) {};
/**
* @param {chrome.cast.SessionRequest} sessionRequest
* @param {Function} sessionListener
* @param {Function} receiverListener
* @param {string=} opt_autoJoinPolicy
* @param {string=} opt_defaultActionPolicy
* @constructor
* @struct
*/
chrome.cast.ApiConfig = function(
sessionRequest,
sessionListener,
receiverListener,
opt_autoJoinPolicy,
opt_defaultActionPolicy) {};
/**
* @param {string} code
* @param {string=} opt_description
* @param {Object=} opt_details
* @constructor
* @struct
*/
chrome.cast.Error = function(code, opt_description, opt_details) {};
/** @type {string} */
chrome.cast.Error.prototype.code;
/** @type {?string} */
chrome.cast.Error.prototype.description;
/** @type {Object} */
chrome.cast.Error.prototype.details;
/**
* @constructor
* @struct
*/
chrome.cast.Receiver = function() {};
/** @const {string} */
chrome.cast.Receiver.prototype.friendlyName;
/**
* @constructor
* @struct
*/
chrome.cast.Session = function() {};
/** @type {string} */
chrome.cast.Session.prototype.sessionId;
/** @type {string} */
chrome.cast.Session.prototype.status;
/** @type {chrome.cast.Receiver} */
chrome.cast.Session.prototype.receiver;
/**
* @param {string} namespace
* @param {Function} listener
*/
chrome.cast.Session.prototype.addMessageListener = function(
namespace, listener) {};
/**
* @param {Function} listener
*/
chrome.cast.Session.prototype.addUpdateListener = function(listener) {};
/**
* @param {string} namespace
* @param {!Object|string} message
* @param {Function} successCallback
* @param {Function} errorCallback
*/
chrome.cast.Session.prototype.sendMessage = function(
namespace, message, successCallback, errorCallback) {};
/**
* @param {Function} successCallback
* @param {Function} errorCallback
*/
chrome.cast.Session.prototype.stop = function(
successCallback, errorCallback) {};
/**
* @param {string} appId
* @constructor
* @struct
*/
chrome.cast.SessionRequest = function(appId) {};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 2 | /** * @license * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview Externs for prefixed fullscreen methods. * @externs */ Document.prototype.msExitFullscreen = function() {}; Document.prototype.webkitExitFullscreen = function() {}; |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | 1 1 | /** * @license * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview Externs for JWK set. * @externs */ /** * A JSON Web Key set. * * @constructor * @struct */ function JWKSet() { /** @type {Array.<JWK>} */ this.keys = []; } /** * A JSON Web Key. * * @constructor * @struct */ function JWK() { /** * A key ID. Any ASCII string. * @type {string} */ this.kid = ''; /** * A key type. One of: * "oct" (symmetric key octect sequence) * "RSA" (RSA key) * "EC" (elliptical curve key) * Use "oct" for clearkey. * @type {string} */ this.kty = ''; /** * A key in base 64. Used with kty="oct". * @type {string} */ this.k = ''; } |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | 2 2 2 2 2 1 1 1 1 1 1 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Externs for MediaKeys based on
* {@link http://goo.gl/blgtZZ EME draft 12 March 2015}
*
* @externs
*/
/**
* @typedef {string}
* 'optional', 'required', 'not-allowed'
*/
var MediaKeysRequirement;
/**
* @typedef {string}
* 'temporary', 'persistent-license', 'persistent-usage-record'
*/
var MediaKeySessionType;
/**
* @typedef {{contentType: string, robustness: string}}
* gjslint: disable=900
*/
var MediaKeySystemMediaCapability;
/**
* @typedef {{
* initDataTypes: (Array.<string>|undefined),
* audioCapabilities: (Array.<!MediaKeySystemMediaCapability>|undefined),
* videoCapabilities: (Array.<!MediaKeySystemMediaCapability>|undefined),
* distinctiveIdentifier: (MediaKeysRequirement|undefined),
* persistentState: (MediaKeysRequirement|undefined),
* sessionTypes: (Array.<MediaKeySessionType>|undefined),
* label: (string|undefined)
* }}
* gjslint: disable=900
*/
var MediaKeySystemConfiguration;
/**
* @param {string} keySystem
* @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
* @return {!Promise.<!MediaKeySystemAccess>}
*/
Navigator.prototype.requestMediaKeySystemAccess =
function(keySystem, supportedConfigurations) {};
/** @const {MediaKeys} */
HTMLMediaElement.prototype.mediaKeys;
/**
* @param {MediaKeys} mediaKeys
* @return {!Promise}
*/
HTMLMediaElement.prototype.setMediaKeys = function(mediaKeys) {};
/** @interface */
function MediaKeySystemAccess() {}
/** @return {!Promise.<!MediaKeys>} */
MediaKeySystemAccess.prototype.createMediaKeys = function() {};
/** @return {!MediaKeySystemConfiguration} */
MediaKeySystemAccess.prototype.getConfiguration = function() {};
/** @const {string} */
MediaKeySystemAccess.prototype.keySystem;
/** @interface */
function MediaKeys() {}
/**
* @param {MediaKeySessionType=} opt_sessionType defaults to "temporary"
* @return {!MediaKeySession}
* @throws {TypeError} if opt_sessionType is invalid.
*/
MediaKeys.prototype.createSession = function(opt_sessionType) {};
/**
* @param {?BufferSource} serverCertificate
* @return {!Promise}
*/
MediaKeys.prototype.setServerCertificate = function(serverCertificate) {};
/**
* @interface
*/
function MediaKeyStatusMap() {}
/** @const {number} */
MediaKeyStatusMap.prototype.size;
/**
* The functor is called with each status and key ID.
* @param {function(string, BufferSource)} fn
*/
MediaKeyStatusMap.prototype.forEach = function(fn) {};
/**
* @param {BufferSource} keyId
* @return {string|undefined}
*/
MediaKeyStatusMap.prototype.get = function(keyId) {};
/**
* @param {BufferSource} keyId
* @return {boolean}
*/
MediaKeyStatusMap.prototype.has = function(keyId) {};
/**
* @interface
* @extends {EventTarget}
*/
function MediaKeySession() {}
/** @const {string} */
MediaKeySession.prototype.sessionId;
/** @const {number} */
MediaKeySession.prototype.expiration;
/** @const {!Promise} */
MediaKeySession.prototype.closed;
/** @const {!MediaKeyStatusMap} */
MediaKeySession.prototype.keyStatuses;
/**
* @param {string} initDataType
* @param {!BufferSource} initData
* @return {!Promise}
*/
MediaKeySession.prototype.generateRequest = function(initDataType, initData) {};
/**
* @param {string} sessionId
* @return {!Promise.<boolean>}}
*/
MediaKeySession.prototype.load = function(sessionId) {};
/**
* @param {?BufferSource} response
* @return {!Promise}
*/
MediaKeySession.prototype.update = function(response) {};
/** @return {!Promise} */
MediaKeySession.prototype.close = function() {};
/** @return {!Promise} */
MediaKeySession.prototype.remove = function() {};
/** @override */
MediaKeySession.prototype.addEventListener =
function(type, listener, useCapture) {};
/** @override */
MediaKeySession.prototype.removeEventListener =
function(type, listener, useCapture) {};
/** @override */
MediaKeySession.prototype.dispatchEvent = function(evt) {};
/**
* @constructor
* @param {string} type
* @param {Object=} opt_eventInitDict
* @extends {Event}
*/
function MediaKeyMessageEvent(type, opt_eventInitDict) {}
/** @const {string} */
MediaKeyMessageEvent.prototype.messageType;
/** @const {!ArrayBuffer} */
MediaKeyMessageEvent.prototype.message;
/** @const {!MediaKeySession} */
MediaKeyMessageEvent.prototype.target;
/**
* @constructor
* @param {string} type
* @param {Object=} opt_eventInitDict
* @extends {Event}
*/
function MediaEncryptedEvent(type, opt_eventInitDict) {}
/** @const {string} */
MediaEncryptedEvent.prototype.initDataType;
/** @const {ArrayBuffer} */
MediaEncryptedEvent.prototype.initData;
/** @const {!HTMLMediaElement} */
MediaEncryptedEvent.prototype.target;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 2 2 2 | /**
* @license
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Externs for MediaSession based on
* {@link https://goo.gl/8QS094 Editor's Draft, 12 January 2017}
*
* @externs
*/
/**
* @constructor
*/
var MediaMetadata = function(options) {};
/** @type {string} */
MediaMetadata.prototype.title;
/** @type {MediaMetadata} */
Navigator.prototype.mediaSession;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | 1 2 2 1 2 2 2 2 2 2 2 1 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Externs for prefixed EME v20140218 as supported by IE11/Edge
* (http://www.w3.org/TR/2014/WD-encrypted-media-20140218).
* @externs
*/
/**
* @constructor
* @param {string} keySystem
*/
function MSMediaKeys(keySystem) {}
/**
* @param {string} keySystem
* @param {string} contentType
* @return {boolean}
*/
MSMediaKeys.isTypeSupported = function(keySystem, contentType) {};
/**
* @param {string} contentType
* @param {Uint8Array} initData
* @param {Uint8Array=} opt_cdmData
* @return {!MSMediaKeySession}
*/
MSMediaKeys.prototype.createSession =
function(contentType, initData, opt_cdmData) {};
/**
* @interface
* @extends {EventTarget}
*/
function MSMediaKeySession() {}
/**
* @param {Uint8Array} message
*/
MSMediaKeySession.prototype.update = function(message) {};
MSMediaKeySession.prototype.close = function() {};
/** @type {MSMediaKeyError} */
MSMediaKeySession.prototype.error;
/** @override */
MSMediaKeySession.prototype.addEventListener =
function(type, listener, useCapture) {};
/** @override */
MSMediaKeySession.prototype.removeEventListener =
function(type, listener, useCapture) {};
/** @override */
MSMediaKeySession.prototype.dispatchEvent = function(evt) {};
/**
* @param {MSMediaKeys} mediaKeys
*/
HTMLMediaElement.prototype.msSetMediaKeys = function(mediaKeys) {};
/** @constructor */
function MSMediaKeyError() {}
/** @type {number} */
MSMediaKeyError.prototype.code;
/** @type {number} */
MSMediaKeyError.prototype.systemCode;
/** @type {number} */
MSMediaKeyError.MS_MEDIA_KEYERR_UNKNOWN;
/** @type {number} */
MSMediaKeyError.MS_MEDIA_KEYERR_CLIENT;
/** @type {number} */
MSMediaKeyError.MS_MEDIA_KEYERR_SERVICE;
/** @type {number} */
MSMediaKeyError.MS_MEDIA_KEYERR_OUTPUT;
/** @type {number} */
MSMediaKeyError.MS_MEDIA_KEYERR_HARDWARECHANGE;
/** @type {number} */
MSMediaKeyError.MS_MEDIA_KEYERR_DOMAIN;
/** @type {number} */
MediaError.prototype.msExtendedCode;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | 2 1 1 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Externs for prefixed EME v0.1b.
* @externs
*/
/**
* @param {string} keySystem
* @param {Uint8Array} key
* @param {Uint8Array} keyId
* @param {string} sessionId
*/
HTMLMediaElement.prototype.webkitAddKey =
function(keySystem, key, keyId, sessionId) {};
/**
* @param {string} keySystem
* @param {string} sessionId
*/
HTMLMediaElement.prototype.webkitCancelKeyRequest =
function(keySystem, sessionId) {};
/**
* @param {string} keySystem
* @param {!Uint8Array} initData
*/
HTMLMediaElement.prototype.webkitGenerateKeyRequest =
function(keySystem, initData) {};
/**
* @param {string} mimeType
* @param {string=} opt_keySystem
* @return {string} '', 'maybe', or 'probably'
* @override the standard one-argument version
*/
HTMLVideoElement.prototype.canPlayType =
function(mimeType, opt_keySystem) {};
/**
* @constructor
* @param {string} type
* @param {Object=} opt_eventInitDict
* @extends {Event}
*/
function MediaKeyEvent(type, opt_eventInitDict) {}
/**
* @type {string}
* @const
*/
MediaKeyEvent.prototype.keySystem;
/**
* @type {string}
* @const
*/
MediaKeyEvent.prototype.sessionId;
/**
* @type {Uint8Array}
* @const
*/
MediaKeyEvent.prototype.initData;
/**
* @type {Uint8Array}
* @const
*/
MediaKeyEvent.prototype.message;
/**
* @type {string}
* @const
*/
MediaKeyEvent.prototype.defaultURL;
/**
* @type {MediaKeyError}
* @const
*/
MediaKeyEvent.prototype.errorCode;
/**
* @type {number}
* @const
*/
MediaKeyEvent.prototype.systemCode;
/**
* @type {!HTMLMediaElement}
* @const
*/
MediaKeyEvent.prototype.target;
/** @constructor */
function MediaKeyError() {}
/** @type {number} */
MediaKeyError.prototype.code;
/** @type {number} */
MediaKeyError.prototype.systemCode;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Externs for TextTrack and TextTrackCue which are
* missing from the closure compiler.
*
* @externs
*/
/** @type {string} */
TextTrack.prototype.label;
/** @type {string}Â */
TextTrackCue.prototype.positionAlign;
/** @type {string}Â */
TextTrackCue.prototype.lineAlign;
/** @type {number|null|string} */
TextTrackCue.prototype.line;
/** @type {string} */
TextTrackCue.prototype.vertical;
/** @type {boolean} */
TextTrackCue.prototype.snapToLines;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Externs for XMLHttpRequest which are missing from the closure
* compiler.
*
* @externs
*/
/** @type {number} */
XMLHttpRequest.UNSENT;
/** @type {number} */
XMLHttpRequest.OPENED;
/** @type {number} */
XMLHttpRequest.HEADERS_RECEIVED;
/** @type {number} */
XMLHttpRequest.LOADING;
/** @type {number} */
XMLHttpRequest.DONE;
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| abr_manager.js | 10% | (1 / 10) | 100% | (0 / 0) | 0% | (0 / 9) | 10% | (1 / 10) | |
| manifest.js | 11.11% | (1 / 9) | 100% | (0 / 0) | 100% | (0 / 0) | 11.11% | (1 / 9) | |
| manifest_parser.js | 20% | (1 / 5) | 100% | (0 / 0) | 0% | (0 / 4) | 20% | (1 / 5) | |
| namespace.js | 100% | (1 / 1) | 100% | (0 / 0) | 100% | (0 / 0) | 100% | (1 / 1) | |
| net.js | 16.67% | (1 / 6) | 100% | (0 / 0) | 100% | (0 / 0) | 16.67% | (1 / 6) | |
| offline.js | 12.5% | (1 / 8) | 100% | (0 / 0) | 100% | (0 / 0) | 12.5% | (1 / 8) | |
| player.js | 6.67% | (1 / 15) | 100% | (0 / 0) | 100% | (0 / 0) | 6.67% | (1 / 15) | |
| text.js | 100% | (1 / 1) | 100% | (0 / 0) | 100% | (0 / 0) | 100% | (1 / 1) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | 2 | /** * @license * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** @externs */ /** * An object which selects Streams for adaptive bit-rate presentations. * * @interface * @exportDoc */ shakaExtern.AbrManager = function() {}; /** * A callback which implementations call to switch streams. * * The first argument is a map of content types to chosen streams. * * The second argument is an optional boolean. If true, all data will be * from the buffer, which will result in a buffering event. * * @typedef {function(!Object.<string, !shakaExtern.Stream>, boolean=)} * @exportDoc */ shakaExtern.AbrManager.SwitchCallback; /** * Initializes the AbrManager. * * @param {shakaExtern.AbrManager.SwitchCallback} switchCallback * @exportDoc */ shakaExtern.AbrManager.prototype.init = function(switchCallback) {}; /** * Chooses one Stream from each StreamSet to switch to. All StreamSets must be * from the same Period. Some StreamSets may be absent in the case of language * changes. * * @param {!Object.<string, !shakaExtern.StreamSet>} streamSetsByType * @return {!Object.<string, shakaExtern.Stream>} * @exportDoc */ shakaExtern.AbrManager.prototype.chooseStreams = function(streamSetsByType) {}; /** * Enables automatic Stream choices from the last StreamSets passed to * chooseStreams(). After this, the AbrManager may call switchCallback() at any * time. * * @exportDoc */ shakaExtern.AbrManager.prototype.enable = function() {}; /** * Disables automatic Stream suggestions. After this, the AbrManager may not * call switchCallback(). * * @exportDoc */ shakaExtern.AbrManager.prototype.disable = function() {}; /** * Notifies the AbrManager that a segment has been downloaded (includes MP4 * SIDX data, WebM Cues data, initialization segments, and media segments). * * @param {number} startTimeMs The wall-clock time, in milliseconds, when the * request began (before any outbound request filters). * @param {number} endTimeMs The wall-clock time, in milliseconds, when the * response ended (after all retries and inbound response filters). * @param {number} numBytes The total number of bytes transferred. * @exportDoc */ shakaExtern.AbrManager.prototype.segmentDownloaded = function( startTimeMs, endTimeMs, numBytes) {}; /** * Stops any background timers and frees any objects held by this instance. * This will only be called after a call to init. * * @exportDoc */ shakaExtern.AbrManager.prototype.stop = function() {}; /** * Gets an estimate of the current bandwidth in bit/sec. This is used by the * Player to generate stats. * * @return {number} * @exportDoc */ shakaExtern.AbrManager.prototype.getBandwidthEstimate = function() {}; /** * Sets the default bandwidth estimate to use if there is not enough data. * * @param {number} estimate The default bandwidth estimate, in bit/sec. * @exportDoc */ shakaExtern.AbrManager.prototype.setDefaultEstimate = function(estimate) {}; |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @externs */
/**
* @typedef {{
* presentationTimeline: !shaka.media.PresentationTimeline,
* periods: !Array.<!shakaExtern.Period>,
* offlineSessionIds: !Array.<string>,
* minBufferTime: number
* }}
*
* @description
* <p>
* A Manifest object describes a collection of streams (segmented audio, video,
* or text data) that share a common timeline. We call the collection of
* streams "the presentation" and their timeline "the presentation timeline".
* A Manifest describes one of two types of presentations: live and
* video-on-demand.
* </p>
*
* <p>
* A live presentation begins at some point in time and either continues
* indefinitely or ends when the presentation stops broadcasting. For a live
* presentation, wall-clock time maps onto the presentation timeline, and the
* current wall-clock time maps to the live-edge (AKA "the current presentation
* time"). In contrast, a video-on-demand presentation exists entirely
* independent of wall-clock time.
* </p>
*
* <p>
* The presentation timeline is divided into one or more Periods, and each of
* these Periods contains its own collection of streams. Periods group their
* streams by type (e.g., 'audio', 'video', or 'text') and logical content, and
* each individual group defines a StreamSet.
* </p>
*
* <p>
* A stream has the same logical content as another stream if the only
* difference between the two is their quality. For example, an SD video stream
* and an HD video stream that depict the same scene have the same logical
* content; whereas an English audio stream and a French audio stream have
* different logical content. The player can automatically switch between
* streams which have the same logical content to adapt to network conditions.
* </p>
*
* @property {!shaka.media.PresentationTimeline} presentationTimeline
* <i>Required.</i> <br>
* The presentation timeline.
* @property {!Array.<!shakaExtern.Period>} periods
* <i>Required.</i> <br>
* The presentation's Periods. There must be at least one Period.
* @property {!Array.<string>} offlineSessionIds
* <i>Defaults to [].</i> <br>
* An array of EME sessions to load for offline playback.
* @property {number} minBufferTime
* <i>Defaults to 0.</i> <br>
* The minimum number of seconds of content that must be buffered before
* playback can begin. Can be overridden by a higher value from the Player
* configuration.
*
* @exportDoc
*/
shakaExtern.Manifest;
/**
* @typedef {{
* startTime: number,
* streamSets: !Array.<shakaExtern.StreamSet>
* }}
*
* @description
* A Period object contains the Streams for part of the presentation.
*
* @property {number} startTime
* <i>Required.</i> <br>
* The Period's start time, in seconds, relative to the start of the
* presentation. The first Period must begin at the start of the
* presentation. The Period ends immediately before the next Period's start
* time or exactly at the end of the presentation timeline. Periods which
* begin after the end of the presentation timeline are ignored.
* @property {!Array.<shakaExtern.StreamSet>} streamSets
* <i>Required.</i> <br>
* The Period's StreamSets. There must be at least one StreamSet.
*
* @exportDoc
*/
shakaExtern.Period;
/**
* @typedef {{
* initData: !Uint8Array,
* initDataType: string
* }}
*
* @description
* Explicit initialization data, which override any initialization data in the
* content. The initDataType values and the formats that they correspond to
* are specified {@link https://goo.gl/TNjYwn here}.
*
* @property {!Uint8Array} initData
* Initialization data in the format indicated by initDataType.
* @property {string} initDataType
* A string to indicate what format initData is in.
*
* @exportDoc
*/
shakaExtern.InitDataOverride;
/**
* @typedef {{
* keySystem: string,
* licenseServerUri: string,
* distinctiveIdentifierRequired: boolean,
* persistentStateRequired: boolean,
* audioRobustness: string,
* videoRobustness: string,
* serverCertificate: Uint8Array,
* initData: Array.<!shakaExtern.InitDataOverride>,
* keyIds: Array.<string>
* }}
*
* @description
* DRM configuration for a single key system.
*
* @property {string} keySystem
* <i>Required.</i> <br>
* The key system, e.g., "com.widevine.alpha".
* @property {string} licenseServerUri
* <i>Filled in by DRM config if missing.</i> <br>
* The license server URI.
* @property {boolean} distinctiveIdentifierRequired
* <i>Defaults to false. Can be filled in by advanced DRM config.</i> <br>
* True if the application requires the key system to support distinctive
* identifiers.
* @property {boolean} persistentStateRequired
* <i>Defaults to false. Can be filled in by advanced DRM config.</i> <br>
* True if the application requires the key system to support persistent
* state, e.g., for persistent license storage.
* @property {string} audioRobustness
* <i>Defaults to '', e.g., no specific robustness required. Can be filled in
* by advanced DRM config.</i> <br>
* A key-system-specific string that specifies a required security level.
* @property {string} videoRobustness
* <i>Defaults to '', e.g., no specific robustness required. Can be filled in
* by advanced DRM config.</i> <br>
* A key-system-specific string that specifies a required security level.
* @property {Uint8Array} serverCertificate
* <i>Defaults to null, e.g., certificate will be requested from the license
* server if required. Can be filled in by advanced DRM config.</i> <br>
* A key-system-specific server certificate used to encrypt license requests.
* Its use is optional and is meant as an optimization to avoid a round-trip
* to request a certificate.
* @property {Array.<!shakaExtern.InitDataOverride>} initData
* <i>Defaults to [], e.g., no override.</i> <br>
* A list of initialization data which override any initialization data found
* in the content. See also shakaExtern.InitDataOverride.
* @property {Array.<string>} keyIds
* <i>Defaults to []</i> <br>
* If not empty, contains the default key IDs for this key system.
* @exportDoc
*/
shakaExtern.DrmInfo;
/**
* @typedef {{
* language: string,
* type: string,
* primary: boolean,
* drmInfos: Array.<!shakaExtern.DrmInfo>,
* streams: !Array.<!shakaExtern.Stream>
* }}
*
* @description
* A StreamSet object contains a set of Streams which have the same type,
* container/format, and logical content. A StreamSet's type and
* container/format define its MIME type.
*
* @property {string} language
* <i>Defaults to '' (i.e., unknown).</i> <br>
* The Streams' language, specified as a language code. <br>
* See {@link https://tools.ietf.org/html/rfc5646} <br>
* See {@link http://www.iso.org/iso/home/standards/language_codes.htm}
* @property {string} type
* <i>Required.</i> <br>
* The Streams' type, e.g., 'audio', 'video', or 'text'.
* @property {boolean} primary
* <i>Defaults to false.</i> <br>
* True indicates that the player should use this StreamSet over others of
* the same type in the same Period. However, the player may use another
* StreamSet to meet application preferences, or to achieve better MIME type
* or DRM compatibility among other StreamSets.
* @property {Array.<!shakaExtern.DrmInfo>} drmInfos
* <i>Defaults to [] (i.e., no DRM).</i> <br>
* An array of DrmInfo objects which describe DRM schemes are compatible with
* the content.
* @property {!Array.<!shakaExtern.Stream>} streams
* <i>Required.</i> <br>
* The StreamSets's Streams. There must be at least one Stream.
*
* @exportDoc
*/
shakaExtern.StreamSet;
/**
* Creates a SegmentIndex; returns a Promise that resolves after the
* SegmentIndex has been created.
*
* @typedef {function(): !Promise}
* @exportDoc
*/
shakaExtern.CreateSegmentIndexFunction;
/**
* Finds the position of the segment for the given time, in seconds, relative
* to the start of a particular Period; returns null if the position of the
* segment could not be determined. Note: the position of a segment is unique
* only among segments within the same Period.
*
* @typedef {function(number): ?number}
* @exportDoc
*/
shakaExtern.FindSegmentPositionFunction;
/**
* Gets the SegmentReference for the segment at the given position; returns
* null if no such SegmentReference exists. Note: the position of a segment is
* unique only among segments within the same Period.
* @typedef {function(number): shaka.media.SegmentReference}
* @exportDoc
*/
shakaExtern.GetSegmentReferenceFunction;
/**
* @typedef {{
* id: number,
* createSegmentIndex: shakaExtern.CreateSegmentIndexFunction,
* findSegmentPosition: shakaExtern.FindSegmentPositionFunction,
* getSegmentReference: shakaExtern.GetSegmentReferenceFunction,
* initSegmentReference: shaka.media.InitSegmentReference,
* presentationTimeOffset: (number|undefined),
* mimeType: string,
* codecs: string,
* frameRate: (number|undefined),
* bandwidth: (number|undefined),
* width: (number|undefined),
* height: (number|undefined),
* kind: (string|undefined),
* encrypted: boolean,
* keyId: ?string,
* language: string,
* allowedByApplication: boolean,
* allowedByKeySystem: boolean
* }}
*
* @description
* A Stream object describes a single stream (segmented media data).
*
* @property {number} id
* <i>Required.</i> <br>
* A unique ID among all Stream objects within the same Manifest.
* @property {shakaExtern.CreateSegmentIndexFunction} createSegmentIndex
* <i>Required.</i> <br>
* Creates the Stream's SegmentIndex (asynchronously).
* @property {shakaExtern.FindSegmentPositionFunction} findSegmentPosition
* <i>Required.</i> <br>
* Finds the position of the segment for the given time. The caller must call
* createSegmentIndex() and wait until the returned Promise resolves before
* calling this function.
* @property {shakaExtern.GetSegmentReferenceFunction} getSegmentReference
* <i>Required.</i> <br>
* Gets the SegmentReference for the segment at the given position. The
* caller must call createSegmentIndex() and wait until the returned Promise
* resolves before calling this function.
* @property {shaka.media.InitSegmentReference} initSegmentReference
* The Stream's initialization segment metadata, or null if the segments are
* self-initializing.
* @property {(number|undefined)} presentationTimeOffset
* <i>Defaults to 0.</i> <br>
* The amount of time, in seconds, that the stream's presentation timestamps
* are offset from the start of the Stream's Period, i.e., this value should
* equal the first presentation timestamp of the first frame/sample in the
* period. <br>
* <br>
* For example, for MP4 based streams, this value should equal the first
* segment's tfdt box's 'baseMediaDecodeTime' field (after it has been
* converted to seconds).
* @property {string} mimeType
* <i>Required.</i> <br>
* The Stream's MIME type, e.g., 'audio/mp4', 'video/webm', or 'text/vtt'.
* @property {string} codecs
* <i>Defaults to '' (i.e., unknown / not needed).</i> <br>
* The Stream's codecs, e.g., 'avc1.4d4015' or 'vp9', which must be
* compatible with the Stream's MIME type. <br>
* See {@link https://tools.ietf.org/html/rfc6381}
* @property {(number|undefined)} frameRate
* <i>Video streams only.</i> <br>
* The Stream's framerate in frames per second
* @property {(number|undefined)} bandwidth
* <i>Audio and video streams only.</i> <br>
* The stream's required bandwidth in bits per second.
* @property {(number|undefined)} width
* <i>Video streams only.</i> <br>
* The stream's width in pixels.
* @property {(number|undefined)} height
* <i>Video streams only.</i> <br>
* The stream's height in pixels.
* @property {(string|undefined)} kind
* <i>Text streams only.</i> <br>
* The kind of text stream. For example, 'captions' or 'subtitles'.
* @see https://goo.gl/k1HWA6
* @property {boolean} encrypted
* <i>Defaults to false.</i><br>
* True if the stream is encrypted.
* @property {?string} keyId
* <i>Defaults to null (i.e., unencrypted or key ID unknown).</i> <br>
* The stream's key ID as a lowercase hex string. This key ID identifies the
* encryption key that the browser (key system) can use to decrypt the
* stream.
* @property {string} language
* The Stream's language, specified as a language code. <br>
* Must be identical to the language of the containing StreamSet.
* @property {boolean} allowedByApplication
* <i>Defaults to true.</i><br>
* Set by the Player to indicate whether the stream is allowed to be played
* by the application.
* @property {boolean} allowedByKeySystem
* <i>Defaults to true.</i><br>
* Set by the Player to indicate whether the stream is allowed to be played
* by the key system.
*
* @exportDoc
*/
shakaExtern.Stream;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | 2 | /** * @license * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** @externs */ /** * Parses media manifests and handles manifest updates. * * Given a URI where the initial manifest is found, a parser will request the * manifest, parse it, and return the resulting Manifest object. * * If the manifest requires updates (e.g. for live media), the parser will use * background timers to update the same Manifest object. * * @interface * @exportDoc */ shakaExtern.ManifestParser = function() {}; /** * A factory for creating the manifest parser. This will be called with 'new'. * This function is registered with shaka.media.ManifestParser to create parser * instances. * * @typedef {function(new:shakaExtern.ManifestParser)} * @exportDoc */ shakaExtern.ManifestParser.Factory; /** * Called by the Player to provide an updated configuration any time the * configuration changes. Will be called at least once before start(). * * @param {shakaExtern.ManifestConfiguration} config * @exportDoc */ shakaExtern.ManifestParser.prototype.configure = function(config) {}; /** * Parses the given manifest data into a Manifest object and starts any * background timers that are needed. This will only be called once. * * @param {string} uri The URI of the manifest. * @param {!shaka.net.NetworkingEngine} networkingEngine The networking engine * to use for network requests. * @param {function(shakaExtern.Period)} filterPeriod A callback to be invoked * on all new Periods so that they can be filtered. * @param {function(!shaka.util.Error)} onError A callback to be invoked on * errors. * @param {function(!Event)} onEvent A callback to be invoked to dispatch events * to the application. * @return {!Promise.<shakaExtern.Manifest>} * @exportDoc */ shakaExtern.ManifestParser.prototype.start = function(uri, networkingEngine, filterPeriod, onError, onEvent) {}; /** * Stops any background timers and frees any objects held by this instance. * This will only be called after a successful call to start. This will only * be called once. * * @return {!Promise} * @exportDoc */ shakaExtern.ManifestParser.prototype.stop = function() {}; |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 1 | /** * @license * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @externs * @namespace * @summary Types and interfaces which are used outside the library. * @exportDoc */ var shakaExtern; |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @externs */
/**
* @typedef {{
* maxAttempts: number,
* baseDelay: number,
* backoffFactor: number,
* fuzzFactor: number,
* timeout: number
* }}
*
* @description
* Parameters for retrying requests.
*
* @property {number} maxAttempts
* The maximum number of times the request should be attempted.
* @property {number} baseDelay
* The delay before the first retry, in milliseconds.
* @property {number} backoffFactor
* The multiplier for successive retry delays.
* @property {number} fuzzFactor
* The maximum amount of fuzz to apply to each retry delay.
* For example, 0.5 means "between 50% below and 50% above the retry delay."
* @property {number} timeout
* The request timeout, in milliseconds. Zero means "unlimited".
*
* @tutorial network-and-buffering-config
*
* @exportDoc
*/
shakaExtern.RetryParameters;
/**
* @typedef {{
* uris: !Array.<string>,
* method: string,
* body: ArrayBuffer,
* headers: !Object.<string, string>,
* allowCrossSiteCredentials: boolean,
* retryParameters: !shakaExtern.RetryParameters
* }}
*
* @description
* Defines a network request. This is passed to one or more request filters
* that may alter the request, then it is passed to a scheme plugin which
* performs the actual operation.
*
* @property {!Array.<string>} uris
* An array of URIs to attempt. They will be tried in the order they are
* given.
* @property {string} method
* The HTTP method to use for the request.
* @property {ArrayBuffer} body
* The body of the request.
* @property {!Object.<string, string>} headers
* A mapping of headers for the request. e.g.: {'HEADER': 'VALUE'}
* @property {boolean} allowCrossSiteCredentials
* Make requests with credentials. This will allow cookies in cross-site
* requests. See <a href="http://goo.gl/YBRKPe">http://goo.gl/YBRKPe</a>.
* @property {!shakaExtern.RetryParameters} retryParameters
* An object used to define how often to make retries.
*
* @exportDoc
*/
shakaExtern.Request;
/**
* @typedef {{
* uri: string,
* data: ArrayBuffer,
* headers: !Object.<string, string>
* }}
*
* @description
* Defines a response object. This includes the response data and header info.
* This is given back from the scheme plugin. This is passed to a response
* filter before being returned from the request call.
*
* @property {string} uri
* The URI which was loaded. Request filters and server redirects can cause
* this to be different from the original request URIs.
* @property {ArrayBuffer} data
* The body of the response.
* @property {!Object.<string, string>} headers
* A map of response headers, if supported by the underlying protocol.
* All keys should be lowercased.
* For HTTP/HTTPS, may not be available cross-origin.
*
* @exportDoc
*/
shakaExtern.Response;
/**
* Defines a plugin that handles a specific scheme.
*
* @typedef {!function(string, shakaExtern.Request):
* !Promise.<shakaExtern.Response>}
* @exportDoc
*/
shakaExtern.SchemePlugin;
/**
* Defines a filter for requests. This filter takes the request and modifies
* it before it is sent to the scheme plugin.
*
* @typedef {!function(shaka.net.NetworkingEngine.RequestType,
* shakaExtern.Request)}
* @exportDoc
*/
shakaExtern.RequestFilter;
/**
* Defines a filter for responses. This filter takes the response and modifies
* it before it is returned.
*
* @typedef {!function(shaka.net.NetworkingEngine.RequestType,
* shakaExtern.Response)}
* @exportDoc
*/
shakaExtern.ResponseFilter;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @externs */
/**
* @typedef {{
* basic: boolean,
* encrypted: !Object.<string, boolean>
* }}
*
* @property {boolean} basic
* True if offline is usable at all.
* @property {!Object.<string, boolean>} encrypted
* A map of key system name to whether it supports offline playback.
* @exportDoc
*/
shakaExtern.OfflineSupport;
/**
* @typedef {{
* trackSelectionCallback:
* function(!Array.<shakaExtern.Track>):!Array.<shakaExtern.Track>,
* progressCallback: function(shakaExtern.StoredContent,number)
* }}
*
* @property {function(!Array.<shakaExtern.Track>):!Array.<shakaExtern.Track>}
* trackSelectionCallback
* Called inside store() to determine which tracks to save from a manifest.
* It is passed an array of Tracks from the manifest and it should return
* an array of the tracks to store. This is called for each Period in the
* manifest (in order).
* @property {function(shakaExtern.StoredContent,number)} progressCallback
* Called inside store() to give progress info back to the app. It is given
* the current manifest being stored and the progress of it being stored.
* @exportDoc
*/
shakaExtern.OfflineConfiguration;
/**
* @typedef {{
* offlineUri: string,
* originalManifestUri: string,
* duration: number,
* size: number,
* tracks: !Array.<shakaExtern.Track>,
* appMetadata: Object
* }}
*
* @property {string} offlineUri
* An offline URI to access the content. This can be passed directly to
* Player.
* @property {string} originalManifestUri
* The original manifest URI of the content stored.
* @property {number} duration
* The duration of the content, in seconds.
* @property {number} size
* The size of the content, in bytes.
* @property {!Array.<shakaExtern.Track>} tracks
* The tracks that are stored. This only lists those found in the first
* Period.
* @property {Object} appMetadata
* The metadata passed to store().
* @exportDoc
*/
shakaExtern.StoredContent;
/**
* @typedef {{
* key: number,
* originalManifestUri: string,
* duration: number,
* size: number,
* periods: !Array.<shakaExtern.PeriodDB>,
* sessionIds: !Array.<string>,
* drmInfo: ?shakaExtern.DrmInfo,
* appMetadata: Object
* }}
*
* @property {number} key
* The key that uniquely identifies the manifest.
* @property {string} originalManifestUri
* The URI that the manifest was originally loaded from.
* @property {number} duration
* The total duration of the media, in seconds.
* @property {number} size
* The total size of all stored segments, in bytes.
* @property {!Array.<shakaExtern.PeriodDB>} periods
* The Periods that are stored.
* @property {!Array.<string>} sessionIds
* The DRM offline session IDs for the media.
* @property {?shakaExtern.DrmInfo} drmInfo
* The DRM info used to initialize EME.
* @property {Object} appMetadata
* A metadata object passed from the application.
*/
shakaExtern.ManifestDB;
/**
* @typedef {{
* startTime: number,
* streams: !Array.<shakaExtern.StreamDB>
* }}
*
* @property {number} startTime
* The start time of the period, in seconds.
* @property {!Array.<shakaExtern.StreamDB>} streams
* The streams that define the Period.
*/
shakaExtern.PeriodDB;
/**
* @typedef {{
* id: number,
* primary: boolean,
* presentationTimeOffset: number,
* contentType: string,
* mimeType: string,
* codecs: string,
* frameRate: (number|undefined),
* kind: (string|undefined),
* language: string,
* width: ?number,
* height: ?number,
* initSegmentUri: ?string,
* encrypted: boolean,
* keyId: ?string,
* segments: !Array.<shakaExtern.SegmentDB>
* }}
*
* @property {number} id
* The unique id of the stream.
* @property {boolean} primary
* Whether the stream set was primary.
* @property {number} presentationTimeOffset
* The presentation time offset of the stream.
* @property {string} contentType
* The type of the stream, 'audio', 'text', or 'video'.
* @property {string} mimeType
* The MIME type of the stream.
* @property {string} codecs
* The codecs of the stream.
* @property {(number|undefined)} frameRate
* The Stream's framerate in frames per second
* @property {(string|undefined)} kind
* The kind of text stream; undefined for audio/video.
* @property {string} language
* The language of the stream; '' for video.
* @property {?number} width
* The width of the stream; null for audio/text.
* @property {?number} height
* The height of the stream; null for audio/text.
* @property {?string} initSegmentUri
* The offline URI where the init segment is found; null if no init segment.
* @property {boolean} encrypted
* Whether this stream is encrypted.
* @property {?string} keyId
* The key ID this stream is encrypted with.
* @property {!Array.<shakaExtern.SegmentDB>} segments
* An array of segments that make up the stream
*/
shakaExtern.StreamDB;
/**
* @typedef {{
* startTime: number,
* endTime: number,
* uri: string
* }}
*
* @property {number} startTime
* The start time of the segment, in seconds from the start of the Period.
* @property {number} endTime
* The end time of the segment, in seconds from the start of the Period.
* @property {string} uri
* The offline URI where the segment is found.
*/
shakaExtern.SegmentDB;
/**
* @typedef {{
* key: number,
* data: !ArrayBuffer,
* manifestKey: number,
* streamNumber: number,
* segmentNumber: number
* }}
*
* @property {number} key
* A key that uniquely describes the segment.
* @property {!ArrayBuffer} data
* The data contents of the segment.
* @property {number} manifestKey
* The key of the manifest this belongs to.
* @property {number} streamNumber
* The index of the stream this belongs to.
* @property {number} segmentNumber
* The index of the segment within the stream.
*/
shakaExtern.SegmentDataDB;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @externs */
/**
* @typedef {{
* timestamp: number,
* id: number,
* type: string,
* fromAdaptation: boolean
* }}
*
* @property {number} timestamp
* The timestamp the choice was made, in seconds since 1970
* (i.e. Date.now() / 1000).
* @property {number} id
* The id of the stream that was chosen.
* @property {string} type
* The type of stream chosen ('audio', 'text', or 'video')
* @property {boolean} fromAdaptation
* True if the choice was made by AbrManager for adaptation; false if it
* was made by the application through selectTrack.
* @exportDoc
*/
shakaExtern.StreamChoice;
/**
* @typedef {{
* width: number,
* height: number,
* streamBandwidth: number,
*
* decodedFrames: number,
* droppedFrames: number,
* estimatedBandwidth: number,
* playTime: number,
* bufferingTime: number,
*
* switchHistory: !Array.<shakaExtern.StreamChoice>
* }}
*
* @description
* Contains statistics and information about the current state of the player.
* This is meant for applications that want to log quality-of-experience (QoE)
* or other stats. These values will reset when load() is called again.
*
* @property {number} width
* The width of the current video track.
* @property {number} height
* The height of the current video track.
* @property {number} streamBandwidth
* The bandwidth required for the current streams (total, in bit/sec).
*
* @property {number} decodedFrames
* The total number of frames decoded by the Player. This may be NaN if this
* is not supported by the browser.
* @property {number} droppedFrames
* The total number of frames dropped by the Player. This may be NaN if this
* is not supported by the browser.
* @property {number} estimatedBandwidth
* The current estimated network bandwidth (in bit/sec).
* @property {number} playTime
* The total time spent in a playing state in seconds.
* @property {number} bufferingTime
* The total time spent in a buffering state in seconds.
*
* @property {!Array.<shakaExtern.StreamChoice>} switchHistory
* A history of the stream changes.
* @exportDoc
*/
shakaExtern.Stats;
/**
* @typedef {{
* id: number,
* active: boolean,
*
* type: string,
* bandwidth: number,
*
* language: string,
* kind: ?string,
* width: ?number,
* height: ?number,
* frameRate: ?number,
* codecs: ?string
* }}
*
* @description
* An object describing a media track. This object should be treated as
* read-only as changing any values does not have any effect. This is the
* public view of the Stream type.
*
* @property {number} id
* The unique ID of the track.
* @property {boolean} active
* If true, this is the track is being streamed (another track may be
* visible/audible in the buffer).
*
* @property {string} type
* The type of track, one of 'audio', 'text', or 'video'.
* @property {number} bandwidth
* The bandwidth required to play the track, in bits/sec.
*
* @property {string} language
* The language of the track, or '' for video tracks. This is the exact
* value provided in the manifest; it may need to be normalized.
* @property {?string} kind
* (only for text tracks) The kind of text track, either 'captions' or
* 'subtitles'.
* @property {?number} width
* (only for video tracks) The width of the track in pixels.
* @property {?number} height
* (only for video tracks) The height of the track in pixels.
* @property {?number} frameRate
* The video framerate provided in the manifest, if present.
* @property {?string} codecs
* The audio/video codecs string provided in the manifest, if present.
* @exportDoc
*/
shakaExtern.Track;
/**
* @typedef {{
* minWidth: number,
* maxWidth: number,
* minHeight: number,
* maxHeight: number,
* minPixels: number,
* maxPixels: number,
*
* minAudioBandwidth: number,
* maxAudioBandwidth: number,
* minVideoBandwidth: number,
* maxVideoBandwidth: number
* }}
*
* @description
* An object describing application restrictions on what tracks can play. All
* restrictions must be fulfilled for a track to be playable. If a track does
* not meet the restrictions, it will not appear in the track list and it will
* not be played.
*
* @property {number} minWidth
* The minimum width of a video track, in pixels.
* @property {number} maxWidth
* The maximum width of a video track, in pixels.
* @property {number} minHeight
* The minimum height of a video track, in pixels.
* @property {number} maxHeight
* The maximum height of a video track, in pixels.
* @property {number} minPixels
* The minimum number of total pixels in a video track (i.e. width * height).
* @property {number} maxPixels
* The maximum number of total pixels in a video track (i.e. width * height).
*
* @property {number} minAudioBandwidth
* The minimum bandwidth of an audio track, in bit/sec.
* @property {number} maxAudioBandwidth
* The maximum bandwidth of an audio track, in bit/sec.
* @property {number} minVideoBandwidth
* The minimum bandwidth of a video track, in bit/sec.
* @property {number} maxVideoBandwidth
* The maximum bandwidth of a video track, in bit/sec.
* @exportDoc
*/
shakaExtern.Restrictions;
/**
* @typedef {{
* persistentState: boolean
* }}
*
* @property {boolean} persistentState
* Whether this key system supports persistent state.
*/
shakaExtern.DrmSupportType;
/**
* @typedef {{
* manifest: !Object.<string, boolean>,
* media: !Object.<string, boolean>,
* drm: !Object.<string, ?shakaExtern.DrmSupportType>
* }}
*
* @description
* An object detailing browser support for various features.
*
* @property {!Object.<string, boolean>} manifest
* A map of supported manifest types.
* The keys are manifest MIME types and file extensions.
* @property {!Object.<string, boolean>} media
* A map of supported media types.
* The keys are media MIME types.
* @property {!Object.<string, ?shakaExtern.DrmSupportType>} drm
* A map of supported key systems.
* The keys are the key system names. The value is null if it is not
* supported. Key systems not probed will not be in this dictionary.
*
* @exportDoc
*/
shakaExtern.SupportType;
/**
* @typedef {{
* schemeIdUri: string,
* value: string,
* timescale: number,
* presentationTimeDelta: number,
* eventDuration: number,
* id: number,
* messageData: Uint8Array
* }}
*
* @description
* Contains information about an EMSG MP4 box.
*
* @property {string} schemeIdUri
* Identifies the message scheme.
* @property {string} value
* Specifies the value for the event.
* @property {number} timescale
* Provides the timescale, in ticks per second,
* for the time and duration fields within this box.
* @property {number} presentationTimeDelta
* Provides the Media Presentation time delta of the media presentation
* time of the event and the earliest presentation time in this segment.
* @property {number} eventDuration
* Provides the duration of event in media presentation time.
* @property {number} id
* A field identifying this instance of the message.
* @property {Uint8Array} messageData
* Body of the message.
* @exportDoc
*/
shakaExtern.EmsgInfo;
/**
* @typedef {function(!Element):Array.<shakaExtern.DrmInfo>}
* @see shakaExtern.DashManifestConfiguration
* @exportDoc
*/
shakaExtern.DashContentProtectionCallback;
/**
* @typedef {{
* distinctiveIdentifierRequired: boolean,
* persistentStateRequired: boolean,
* videoRobustness: string,
* audioRobustness: string,
* serverCertificate: Uint8Array
* }}
*
* @property {boolean} distinctiveIdentifierRequired
* <i>Defaults to false.</i> <br>
* True if the application requires the key system to support distinctive
* identifiers.
* @property {boolean} persistentStateRequired
* <i>Defaults to false.</i> <br>
* True if the application requires the key system to support persistent
* state, e.g., for persistent license storage.
* @property {string} videoRobustness
* A key-system-specific string that specifies a required security level for
* video.
* <i>Defaults to '', i.e., no specific robustness required.</i> <br>
* @property {string} audioRobustness
* A key-system-specific string that specifies a required security level for
* audio.
* <i>Defaults to '', i.e., no specific robustness required.</i> <br>
* @property {Uint8Array} serverCertificate
* <i>Defaults to null, i.e., certificate will be requested from the license
* server if required.</i> <br>
* A key-system-specific server certificate used to encrypt license requests.
* Its use is optional and is meant as an optimization to avoid a round-trip
* to request a certificate.
*
* @exportDoc
*/
shakaExtern.AdvancedDrmConfiguration;
/**
* @typedef {{
* retryParameters: shakaExtern.RetryParameters,
* servers: !Object.<string, string>,
* clearKeys: !Object.<string, string>,
* advanced: Object.<string, shakaExtern.AdvancedDrmConfiguration>
* }}
*
* @property {shakaExtern.RetryParameters} retryParameters
* Retry parameters for license requests.
* @property {!Object.<string, string>} servers
* <i>Required for all but the clear key CDM.</i> <br>
* A dictionary which maps key system IDs to their license servers.
* For example, {'com.widevine.alpha': 'http://example.com/drm'}.
* @property {!Object.<string, string>} clearKeys
* <i>Forces the use of the Clear Key CDM.</i>
* A map of key IDs (hex) to keys (hex).
* @property {Object.<string, shakaExtern.AdvancedDrmConfiguration>} advanced
* <i>Optional.</i> <br>
* A dictionary which maps key system IDs to advanced DRM configuration for
* those key systems.
*
* @exportDoc
*/
shakaExtern.DrmConfiguration;
/**
* @typedef {{
* customScheme: shakaExtern.DashContentProtectionCallback,
* clockSyncUri: string
* }}
*
* @property {shakaExtern.DashContentProtectionCallback} customScheme
* If given, invoked by a DASH manifest parser to interpret custom or
* non-standard DRM schemes found in the manifest. The argument is a
* ContentProtection node. Return null if not recognized.
* @property {string} clockSyncUri
* A default clock sync URI to be used with live streams which do not
* contain any clock sync information. The "Date" header from this URI
* will be used to determine the current time.
*
* @exportDoc
*/
shakaExtern.DashManifestConfiguration;
/**
* @typedef {{
* retryParameters: shakaExtern.RetryParameters,
* dash: shakaExtern.DashManifestConfiguration
* }}
*
* @property {shakaExtern.RetryParameters} retryParameters
* Retry parameters for manifest requests.
* @property {shakaExtern.DashManifestConfiguration} dash
* Advanced parameters used by the DASH manifest parser.
*
* @exportDoc
*/
shakaExtern.ManifestConfiguration;
/**
* @typedef {{
* retryParameters: shakaExtern.RetryParameters,
* rebufferingGoal: number,
* bufferingGoal: number,
* bufferBehind: number,
* ignoreTextStreamFailures: boolean,
* useRelativeCueTimestamps: boolean
* }}
*
* @description
* The StreamingEngine's configuration options.
*
* @property {shakaExtern.RetryParameters} retryParameters
* Retry parameters for segment requests.
* @property {number} rebufferingGoal
* The minimum number of seconds of content that the StreamingEngine must
* buffer before it can begin playback or can continue playback after it has
* entered into a buffering state (i.e., after it has depleted one more
* more of its buffers).
* @property {number} bufferingGoal
* The number of seconds of content that the StreamingEngine will attempt to
* buffer ahead of the playhead. This value must be greater than or equal to
* the rebuffering goal.
* @property {number} bufferBehind
* The maximum number of seconds of content that the StreamingEngine will keep
* in buffer behind the playhead when it appends a new media segment.
* The StreamingEngine will evict content to meet this limit.
* @property {boolean} ignoreTextStreamFailures
* If true, the player will ignore text stream failures and proceed to play
* other streams.
* @property {boolean} useRelativeCueTimestamps
* If true, WebVTT cue timestamps will be treated as relative to the start
* time of the VTT segment. Defaults to false.
* @exportDoc
*/
shakaExtern.StreamingConfiguration;
/**
* @typedef {{
* manager: shakaExtern.AbrManager,
* enabled: boolean,
* defaultBandwidthEstimate: number
* }}
*
* @property {shakaExtern.AbrManager} manager
* The AbrManager instance.
* @property {boolean} enabled
* If true, enable adaptation by the current AbrManager. Defaults to true.
* @property {number} defaultBandwidthEstimate
* The default bandwidth estimate to use if there is not enough data, in
* bit/sec.
* @exportDoc
*/
shakaExtern.AbrConfiguration;
/**
* @typedef {{
* drm: shakaExtern.DrmConfiguration,
* manifest: shakaExtern.ManifestConfiguration,
* streaming: shakaExtern.StreamingConfiguration,
* abr: shakaExtern.AbrConfiguration,
* preferredAudioLanguage: string,
* preferredTextLanguage: string,
* restrictions: shakaExtern.Restrictions
* }}
*
* @property {shakaExtern.DrmConfiguration} drm
* DRM configuration and settings.
* @property {shakaExtern.ManifestConfiguration} manifest
* Manifest configuration and settings.
* @property {shakaExtern.StreamingConfiguration} streaming
* Streaming configuration and settings.
* @property {shakaExtern.AbrConfiguration} abr
* ABR configuration and settings.
* @property {string} preferredAudioLanguage
* The preferred language to use for audio tracks. If not given it will use
* the 'main' track.
* Changing this during playback will cause the language selection algorithm
* to run again, and may change the active audio track.
* @property {string} preferredTextLanguage
* The preferred language to use for text tracks. If a matching text track
* is found, and the selected audio and text tracks have different languages,
* the text track will be shown.
* Changing this during playback will cause the language selection algorithm
* to run again, and may change the active text track.
* @property {shakaExtern.Restrictions} restrictions
* The application restrictions to apply to the tracks. The track must
* meet all the restrictions to be playable.
* @exportDoc
*/
shakaExtern.PlayerConfiguration;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @externs */
/**
* Parses a text buffer into an array of cues.
*
* @typedef {function(ArrayBuffer, number, ?number,
* ?number, boolean):!Array.<!TextTrackCue>}
* @exportDoc
*/
shakaExtern.TextParserPlugin;
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| player.js | 0.19% | (1 / 537) | 0% | (0 / 210) | 0% | (0 / 84) | 0.19% | (1 / 528) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.Player');
goog.require('goog.asserts');
goog.require('shaka.abr.EwmaBandwidthEstimator');
goog.require('shaka.abr.SimpleAbrManager');
goog.require('shaka.log');
goog.require('shaka.media.DrmEngine');
goog.require('shaka.media.ManifestParser');
goog.require('shaka.media.MediaSourceEngine');
goog.require('shaka.media.Playhead');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.media.StreamingEngine');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.CancelableChain');
goog.require('shaka.util.ConfigUtils');
goog.require('shaka.util.Error');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.MapUtils');
goog.require('shaka.util.PublicPromise');
goog.require('shaka.util.StreamUtils');
/**
* Construct a Player.
*
* @param {!HTMLMediaElement} video Any existing TextTracks attached to this
* element that were not created by Shaka will be disabled. A new TextTrack
* may be created to display captions or subtitles.
* @param {function(shaka.Player)=} opt_dependencyInjector Optional callback
* which is called to inject mocks into the Player. Used for testing.
*
* @constructor
* @struct
* @implements {shaka.util.IDestroyable}
* @extends {shaka.util.FakeEventTarget}
* @export
*/
shaka.Player = function(video, opt_dependencyInjector) {
shaka.util.FakeEventTarget.call(this);
/** @private {boolean} */
this.destroyed_ = false;
/** @private {HTMLMediaElement} */
this.video_ = video;
/** @private {TextTrack} */
this.textTrack_ = null;
/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/** @private {shakaExtern.AbrManager} */
this.defaultAbrManager_ = new shaka.abr.SimpleAbrManager();
/** @private {shaka.net.NetworkingEngine} */
this.networkingEngine_ = null;
/** @private {shaka.media.DrmEngine} */
this.drmEngine_ = null;
/** @private {MediaSource} */
this.mediaSource_ = null;
/** @private {shaka.media.MediaSourceEngine} */
this.mediaSourceEngine_ = null;
/** @private {Promise} */
this.mediaSourceOpen_ = null;
/** @private {shaka.media.Playhead} */
this.playhead_ = null;
/** @private {shaka.media.StreamingEngine} */
this.streamingEngine_ = null;
/** @private {shakaExtern.ManifestParser} */
this.parser_ = null;
/** @private {?shakaExtern.Manifest} */
this.manifest_ = null;
/** @private {?string} */
this.manifestUri_ = null;
/**
* Contains an ID for use with creating streams. The manifest parser should
* start with small IDs, so this starts with a large one.
* @private {number}
*/
this.nextExternalStreamId_ = 1e9;
/** @private {!Array.<number>} */
this.loadingTextStreamIds_ = [];
/** @private {boolean} */
this.buffering_ = false;
/** @private {boolean} */
this.switchingPeriods_ = true;
/** @private {shaka.util.CancelableChain} */
this.loadChain_ = null;
/** @private {Promise} */
this.unloadChain_ = null;
/**
* @private {!Object.<string, {
* stream: shakaExtern.Stream,
* clearBuffer: boolean
* }>}
*/
this.deferredSwitches_ = {};
/** @private {?shakaExtern.PlayerConfiguration} */
this.config_ = this.defaultConfig_();
/** @private {{width: number, height: number}} */
this.maxHwRes_ = { width: Infinity, height: Infinity };
/** @private {!Array.<shakaExtern.StreamChoice>} */
this.switchHistory_ = [];
/** @private {number} */
this.playTime_ = 0;
/** @private {number} */
this.bufferingTime_ = 0;
/** @private {number} */
this.lastStatUpdateTimestamp_ = 0;
if (opt_dependencyInjector)
opt_dependencyInjector(this);
this.networkingEngine_ = this.createNetworkingEngine();
this.initialize_();
};
goog.inherits(shaka.Player, shaka.util.FakeEventTarget);
/**
* After destruction, a Player object cannot be used again.
*
* @override
* @export
*/
shaka.Player.prototype.destroy = function() {
this.destroyed_ = true;
var cancelation = Promise.resolve();
if (this.loadChain_) {
// A load is in progress. Cancel it.
cancelation = this.loadChain_.cancel(new shaka.util.Error(
shaka.util.Error.Category.PLAYER,
shaka.util.Error.Code.LOAD_INTERRUPTED));
}
return cancelation.then(function() {
var p = Promise.all([
// We need to destroy the current fields as well as waiting for an
// existing unload to complete. It is fine to call destroyStreaming_ if
// there is an unload since it resets the fields immediately.
this.unloadChain_,
this.destroyStreaming_(),
this.eventManager_ ? this.eventManager_.destroy() : null,
this.networkingEngine_ ? this.networkingEngine_.destroy() : null
]);
this.video_ = null;
this.textTrack_ = null;
this.eventManager_ = null;
this.defaultAbrManager_ = null;
this.networkingEngine_ = null;
this.config_ = null;
return p;
}.bind(this));
};
/**
* @define {string} A version number taken from git at compile time.
*/
goog.define('GIT_VERSION', 'v2.0.8-debug');
/**
* @const {string}
* @export
*/
shaka.Player.version = GIT_VERSION;
/**
* @event shaka.Player.ErrorEvent
* @description Fired when a playback error occurs.
* @property {string} type
* 'error'
* @property {!shaka.util.Error} detail
* An object which contains details on the error. The error's 'category' and
* 'code' properties will identify the specific error that occured. In an
* uncompiled build, you can also use the 'message' and 'stack' properties
* to debug.
* @exportDoc
*/
/**
* @event shaka.Player.EmsgEvent
* @description Fired when a non-typical emsg is found in a segment.
* @property {string} type
* 'emsg'
* @property {shakaExtern.EmsgInfo} detail
* An object which contains the content of the emsg box.
* @exportDoc
*/
/**
* @event shaka.Player.BufferingEvent
* @description Fired when the player's buffering state changes.
* @property {string} type
* 'buffering'
* @property {boolean} buffering
* True when the Player enters the buffering state.
* False when the Player leaves the buffering state.
* @exportDoc
*/
/**
* @event shaka.Player.LoadingEvent
* @description Fired when the player begins loading.
* Used by the Cast receiver to determine idle state.
* @property {string} type
* 'loading'
* @exportDoc
*/
/**
* @event shaka.Player.UnloadingEvent
* @description Fired when the player unloads or fails to load.
* Used by the Cast receiver to determine idle state.
* @property {string} type
* 'unloading'
* @exportDoc
*/
/**
* @event shaka.Player.TextTrackVisibilityEvent
* @description Fired when text track visibility changes.
* @property {string} type
* 'texttrackvisibility'
* @exportDoc
*/
/**
* @event shaka.Player.TracksChangedEvent
* @description Fired when the list of tracks changes. For example, this will
* happen when changing periods or when track restrictions change.
* @property {string} type
* 'trackschanged'
* @exportDoc
*/
/**
* @event shaka.Player.AdaptationEvent
* @description Fired when an automatic adaptation causes the active tracks
* to change. Does not fire when the application calls selectTrack().
* @property {string} type
* 'adaptation'
* @exportDoc
*/
/** @private {!Object.<string, function():*>} */
shaka.Player.supportPlugins_ = {};
/**
* Registers a plugin callback that will be called with support(). The
* callback will return the value that will be stored in the return value from
* support().
*
* @param {string} name
* @param {function():*} callback
* @export
*/
shaka.Player.registerSupportPlugin = function(name, callback) {
shaka.Player.supportPlugins_[name] = callback;
};
/**
* Return whether the browser provides basic support. If this returns false,
* Shaka Player cannot be used at all. In this case, do not construct a Player
* instance and do not use the library.
*
* @return {boolean}
* @export
*/
shaka.Player.isBrowserSupported = function() {
// Basic features needed for the library to be usable.
var basic = !!window.Promise && !!window.Uint8Array &&
!!Array.prototype.forEach;
return basic &&
shaka.media.MediaSourceEngine.isBrowserSupported() &&
shaka.media.DrmEngine.isBrowserSupported();
};
/**
* Probes the browser to determine what features are supported. This makes a
* number of requests to EME/MSE/etc which may result in user prompts. This
* should only be used for diagnostics.
*
* NOTE: This may show a request to the user for permission.
*
* @see https://goo.gl/ovYLvl
* @return {!Promise.<shakaExtern.SupportType>}
* @export
*/
shaka.Player.probeSupport = function() {
goog.asserts.assert(shaka.Player.isBrowserSupported(),
'Must have basic support');
return shaka.media.DrmEngine.probeSupport().then(function(drm) {
var manifest = shaka.media.ManifestParser.probeSupport();
var media = shaka.media.MediaSourceEngine.probeSupport();
var ret = {
manifest: manifest,
media: media,
drm: drm
};
var plugins = shaka.Player.supportPlugins_;
for (var name in plugins) {
ret[name] = plugins[name]();
}
return ret;
});
};
/**
* Load a manifest.
*
* @param {string} manifestUri
* @param {number=} opt_startTime Optional start time, in seconds, to begin
* playback. Defaults to 0 for VOD and to the live edge for live.
* @param {shakaExtern.ManifestParser.Factory=} opt_manifestParserFactory
* Optional manifest parser factory to override auto-detection or use an
* unregistered parser.
* @return {!Promise} Resolved when the manifest has been loaded and playback
* has begun; rejected when an error occurs or the call was interrupted by
* destroy(), unload() or another call to load().
* @export
*/
shaka.Player.prototype.load = function(manifestUri, opt_startTime,
opt_manifestParserFactory) {
var unloadPromise = this.unload();
var loadChain = new shaka.util.CancelableChain();
this.loadChain_ = loadChain;
this.dispatchEvent(new shaka.util.FakeEvent('loading'));
return loadChain.then(function() {
return unloadPromise;
}).then(function() {
goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed');
return shaka.media.ManifestParser.getFactory(
manifestUri,
this.networkingEngine_,
this.config_.manifest.retryParameters,
opt_manifestParserFactory);
}.bind(this)).then(function(factory) {
this.parser_ = new factory();
this.parser_.configure(this.config_.manifest);
goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed');
return this.parser_.start(
manifestUri,
this.networkingEngine_,
this.filterPeriod_.bind(this),
this.onError_.bind(this),
this.onEvent_.bind(this));
}.bind(this)).then(function(manifest) {
if (manifest.periods.length == 0) {
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.NO_PERIODS);
}
this.manifest_ = manifest;
this.manifestUri_ = manifestUri;
this.drmEngine_ = this.createDrmEngine();
this.drmEngine_.configure(this.config_.drm);
return this.drmEngine_.init(manifest, false /* isOffline */);
}.bind(this)).then(function() {
// Re-filter the manifest after DRM has been initialized.
this.manifest_.periods.forEach(this.filterPeriod_.bind(this));
this.lastStatUpdateTimestamp_ = Date.now() / 1000;
// Wait for MediaSource to open before continuing.
return Promise.all([
this.drmEngine_.attach(this.video_),
this.mediaSourceOpen_
]);
}.bind(this)).then(function() {
this.config_.abr.manager.init(this.switch_.bind(this));
// MediaSource is open, so create the Playhead, MediaSourceEngine, and
// StreamingEngine.
this.playhead_ = this.createPlayhead(opt_startTime);
this.mediaSourceEngine_ = this.createMediaSourceEngine();
this.streamingEngine_ = this.createStreamingEngine();
this.streamingEngine_.configure(this.config_.streaming);
return this.streamingEngine_.init();
}.bind(this)).then(function() {
// Re-filter the manifest after streams have been chosen.
this.manifest_.periods.forEach(this.filterPeriod_.bind(this));
// Dispatch a 'trackschanged' event now that all initial filtering is done.
this.onTracksChanged_();
// Since the first streams just became active, send an adaptation event.
this.onAdaptation_();
this.loadChain_ = null;
}.bind(this)).finalize().catch(function(error) {
goog.asserts.assert(error instanceof shaka.util.Error,
'Wrong error type!');
shaka.log.debug('load() failed:', error);
// If we haven't started another load, clear the loadChain_ member.
if (this.loadChain_ == loadChain) {
this.loadChain_ = null;
this.dispatchEvent(new shaka.util.FakeEvent('unloading'));
}
return Promise.reject(error);
}.bind(this));
};
/**
* Creates a new instance of DrmEngine. This can be replaced by tests to
* create fake instances instead.
*
* @return {!shaka.media.DrmEngine}
*/
shaka.Player.prototype.createDrmEngine = function() {
goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed');
return new shaka.media.DrmEngine(
this.networkingEngine_,
this.onError_.bind(this),
this.onKeyStatus_.bind(this));
};
/**
* Creates a new instance of NetworkingEngine. This can be replaced by tests
* to create fake instances instead.
*
* @return {!shaka.net.NetworkingEngine}
*/
shaka.Player.prototype.createNetworkingEngine = function() {
return new shaka.net.NetworkingEngine(this.onSegmentDownloaded_.bind(this));
};
/**
* Creates a new instance of Playhead. This can be replaced by tests to create
* fake instances instead.
*
* @param {number=} opt_startTime
* @return {!shaka.media.Playhead}
*/
shaka.Player.prototype.createPlayhead = function(opt_startTime) {
var timeline = this.manifest_.presentationTimeline;
var rebufferingGoal = shaka.media.StreamingEngine.getRebufferingGoal(
this.manifest_, this.config_.streaming, 1 /* scaleFactor */);
return new shaka.media.Playhead(
this.video_, timeline, rebufferingGoal, opt_startTime || null,
this.onBuffering_.bind(this), this.onSeek_.bind(this));
};
/**
* Create and open MediaSource. Potentially slow.
*
* @return {!Promise}
*/
shaka.Player.prototype.createMediaSource = function() {
this.mediaSource_ = new MediaSource();
var ret = new shaka.util.PublicPromise();
this.eventManager_.listen(this.mediaSource_, 'sourceopen', ret.resolve);
this.video_.src = window.URL.createObjectURL(this.mediaSource_);
return ret;
};
/**
* Creates a new instance of MediaSourceEngine. This can be replaced by tests
* to create fake instances instead.
*
* @return {!shaka.media.MediaSourceEngine}
*/
shaka.Player.prototype.createMediaSourceEngine = function() {
return new shaka.media.MediaSourceEngine(
this.video_, this.mediaSource_, this.textTrack_);
};
/**
* Creates a new instance of StreamingEngine. This can be replaced by tests
* to create fake instances instead.
*
* @return {!shaka.media.StreamingEngine}
*/
shaka.Player.prototype.createStreamingEngine = function() {
goog.asserts.assert(this.playhead_, 'Must not be destroyed');
goog.asserts.assert(this.mediaSourceEngine_, 'Must not be destroyed');
goog.asserts.assert(this.manifest_, 'Must not be destroyed');
return new shaka.media.StreamingEngine(
this.playhead_, this.mediaSourceEngine_, this.networkingEngine_,
this.manifest_, this.onChooseStreams_.bind(this),
this.canSwitch_.bind(this), this.onError_.bind(this));
};
/**
* Configure the Player instance.
*
* The config object passed in need not be complete. It will be merged with
* the existing Player configuration.
*
* Config keys and types will be checked. If any problems with the config
* object are found, errors will be reported through logs.
*
* @param {!Object} config This should follow the form of
* {@link shakaExtern.PlayerConfiguration}, but you may omit any field you do
* not wish to change.
* @export
*/
shaka.Player.prototype.configure = function(config) {
goog.asserts.assert(this.config_, 'Config must not be null!');
if (config.abr && config.abr.manager &&
config.abr.manager != this.config_.abr.manager) {
this.config_.abr.manager.stop();
config.abr.manager.init(this.switch_.bind(this));
}
shaka.util.ConfigUtils.mergeConfigObjects(
this.config_, config, this.defaultConfig_(), this.configOverrides_(), '');
this.applyConfig_();
};
/**
* Apply config changes.
* @private
*/
shaka.Player.prototype.applyConfig_ = function() {
if (this.parser_) {
this.parser_.configure(this.config_.manifest);
}
if (this.drmEngine_) {
this.drmEngine_.configure(this.config_.drm);
}
if (this.streamingEngine_) {
this.streamingEngine_.configure(this.config_.streaming);
// Need to apply the restrictions to every period.
try {
// this.filterPeriod_() may throw.
this.manifest_.periods.forEach(this.filterPeriod_.bind(this));
} catch (error) {
this.onError_(error);
}
// May need to choose new streams.
shaka.log.debug('Choosing new streams after changing configuration');
var period = this.streamingEngine_.getCurrentPeriod();
this.chooseStreamsAndSwitch_(period);
}
// Simply enable/disable ABR with each call, since multiple calls to these
// methods have no effect.
if (this.config_.abr.enabled && !this.switchingPeriods_) {
this.config_.abr.manager.enable();
} else {
this.config_.abr.manager.disable();
}
this.config_.abr.manager.setDefaultEstimate(
this.config_.abr.defaultBandwidthEstimate);
};
/**
* Return a copy of the current configuration. Modifications of the returned
* value will not affect the Player's active configuration. You must call
* player.configure() to make changes.
*
* @return {shakaExtern.PlayerConfiguration}
* @export
*/
shaka.Player.prototype.getConfiguration = function() {
goog.asserts.assert(this.config_, 'Config must not be null!');
var ret = this.defaultConfig_();
shaka.util.ConfigUtils.mergeConfigObjects(
ret, this.config_, this.defaultConfig_(), this.configOverrides_(), '');
return ret;
};
/**
* Reset configuration to default.
* @export
*/
shaka.Player.prototype.resetConfiguration = function() {
var config = this.defaultConfig_();
if (config.abr && config.abr.manager &&
config.abr.manager != this.config_.abr.manager) {
this.config_.abr.manager.stop();
config.abr.manager.init(this.switch_.bind(this));
}
// Don't call mergeConfigObjects_(), since that would not reset open-ended
// dictionaries like drm.servers.
this.config_ = this.defaultConfig_();
this.applyConfig_();
};
/**
* @return {HTMLMediaElement} A reference to the HTML Media Element passed
* in during initialization.
* @export
*/
shaka.Player.prototype.getMediaElement = function() {
return this.video_;
};
/**
* @return {shaka.net.NetworkingEngine} A reference to the Player's networking
* engine. Applications may use this to make requests through Shaka's
* networking plugins.
* @export
*/
shaka.Player.prototype.getNetworkingEngine = function() {
return this.networkingEngine_;
};
/**
* @return {?string} If a manifest is loaded, returns the manifest URI given in
* the last call to load(). Otherwise, returns null.
* @export
*/
shaka.Player.prototype.getManifestUri = function() {
return this.manifestUri_;
};
/**
* @return {boolean} True if the current stream is live. False otherwise.
* @export
*/
shaka.Player.prototype.isLive = function() {
return this.manifest_ ?
this.manifest_.presentationTimeline.isLive() :
false;
};
/**
* @return {boolean} True if the current stream is in-progress VOD.
* False otherwise.
* @export
*/
shaka.Player.prototype.isInProgress = function() {
return this.manifest_ ?
this.manifest_.presentationTimeline.isInProgress() :
false;
};
/**
* Get the seekable range for the current stream.
* @return {{start: number, end: number}}
* @export
*/
shaka.Player.prototype.seekRange = function() {
var start = 0;
var end = 0;
if (this.manifest_) {
var timeline = this.manifest_.presentationTimeline;
start = timeline.getSegmentAvailabilityStart();
end = timeline.getSeekRangeEnd();
}
return {'start': start, 'end': end};
};
/**
* Get the key system currently being used by EME. This returns the empty
* string if not using EME.
*
* @return {string}
* @export
*/
shaka.Player.prototype.keySystem = function() {
return this.drmEngine_ ? this.drmEngine_.keySystem() : '';
};
/**
* Get the DrmInfo used to initialize EME. This returns null when not using
* EME.
*
* @return {?shakaExtern.DrmInfo}
* @export
*/
shaka.Player.prototype.drmInfo = function() {
return this.drmEngine_ ? this.drmEngine_.getDrmInfo() : null;
};
/**
* @return {boolean} True if the Player is in a buffering state.
* @export
*/
shaka.Player.prototype.isBuffering = function() {
return this.buffering_;
};
/**
* Unload the current manifest and make the Player available for re-use.
*
* @return {!Promise} Resolved when streaming has stopped and the previous
* content, if any, has been unloaded.
* @export
*/
shaka.Player.prototype.unload = function() {
if (this.destroyed_) return Promise.resolve();
this.dispatchEvent(new shaka.util.FakeEvent('unloading'));
var p = Promise.resolve();
if (this.loadChain_) {
// A load is in progress, cancel it.
var interrupt = new shaka.util.Error(
shaka.util.Error.Category.PLAYER,
shaka.util.Error.Code.LOAD_INTERRUPTED);
p = this.loadChain_.cancel(interrupt);
}
return p.then(function() {
// If there is an existing unload operation, use that.
if (!this.unloadChain_) {
this.unloadChain_ = this.resetStreaming_().then(function() {
this.unloadChain_ = null;
}.bind(this));
}
return this.unloadChain_;
}.bind(this));
};
/**
* Gets the current effective playback rate. If using trick play, it will
* return the current trick play rate; otherwise, it will return the video
* playback rate.
* @return {number}
* @export
*/
shaka.Player.prototype.getPlaybackRate = function() {
return this.playhead_ ? this.playhead_.getPlaybackRate() : 0;
};
/**
* Skip through the content without playing. Simulated using repeated seeks.
*
* Trick play will be canceled automatically if the playhead hits the beginning
* or end of the seekable range for the content.
*
* @param {number} rate The playback rate to simulate. For example, a rate of
* 2.5 would result in 2.5 seconds of content being skipped every second.
* To trick-play backward, use a negative rate.
* @export
*/
shaka.Player.prototype.trickPlay = function(rate) {
if (this.playhead_)
this.playhead_.setPlaybackRate(rate);
};
/**
* Cancel trick-play.
* @export
*/
shaka.Player.prototype.cancelTrickPlay = function() {
if (this.playhead_)
this.playhead_.setPlaybackRate(1);
};
/**
* Return a list of audio, video, and text tracks available for the current
* Period. If there are multiple Periods, then you must seek to the Period
* before being able to switch.
*
* @return {!Array.<shakaExtern.Track>}
* @export
*/
shaka.Player.prototype.getTracks = function() {
if (!this.streamingEngine_)
return [];
var activeStreams = this.streamingEngine_.getActiveStreams();
var period = this.streamingEngine_.getCurrentPeriod();
return shaka.util.StreamUtils.getTracks(period, activeStreams)
.filter(function(track) {
// Don't show any tracks that are being loaded still.
return this.loadingTextStreamIds_.indexOf(track.id) < 0;
}.bind(this));
};
/**
* Select a specific track. For audio or video, this disables adaptation.
* Note that AdaptationEvents are not fired for manual track selections.
*
* @param {shakaExtern.Track} track
* @param {boolean=} opt_clearBuffer
* @export
*/
shaka.Player.prototype.selectTrack = function(track, opt_clearBuffer) {
if (!this.streamingEngine_)
return;
var period = this.streamingEngine_.getCurrentPeriod();
var data = shaka.util.StreamUtils.findStreamForTrack(period, track);
if (!data) {
shaka.log.error('Unable to find the track with id "' + track.id +
'"; did we change Periods?');
return;
}
var stream = data.stream;
// Double check that the track is allowed to be played.
if (!stream.allowedByApplication || !stream.allowedByKeySystem) {
shaka.log.error('Unable to switch to track with id "' + track.id +
'" because it is restricted.');
return;
}
// Add an entry to the history.
this.switchHistory_.push({
timestamp: Date.now() / 1000,
id: stream.id,
type: track.type,
fromAdaptation: false
});
var streamsToSwitch = {};
streamsToSwitch[track.type] = stream;
if (track.type != 'text') {
// Save current text stream to insure that it doesn't get overridden
// by a default one inside shaka.Player.configure()
var activeStreams = this.streamingEngine_.getActiveStreams();
var currentTextStream = activeStreams['text'];
// Disable ABR for anything other than text track changes.
var config = {abr: {enabled: false}};
this.configure(config);
if (currentTextStream) {
streamsToSwitch['text'] = currentTextStream;
}
}
this.deferredSwitch_(streamsToSwitch, opt_clearBuffer);
};
/**
* @return {boolean} True if the current text track is visible.
* @export
*/
shaka.Player.prototype.isTextTrackVisible = function() {
return this.textTrack_.mode == 'showing';
};
/**
* Set the visibility of the current text track, if any.
*
* @param {boolean} on
* @export
*/
shaka.Player.prototype.setTextTrackVisibility = function(on) {
this.textTrack_.mode = on ? 'showing' : 'hidden';
this.onTextTrackVisibility_();
};
/**
* Return playback and adaptation stats.
*
* @return {shakaExtern.Stats}
* @export
*/
shaka.Player.prototype.getStats = function() {
this.updateStats_();
var video = {};
var audio = {};
var videoInfo = this.video_ && this.video_.getVideoPlaybackQuality ?
this.video_.getVideoPlaybackQuality() :
{};
if (this.streamingEngine_) {
var activeStreams = this.streamingEngine_.getActiveStreams();
video = activeStreams['video'] || {};
audio = activeStreams['audio'] || {};
}
return {
width: video.width || 0,
height: video.height || 0,
streamBandwidth: (video.bandwidth + audio.bandwidth) || 0,
decodedFrames: Number(videoInfo.totalVideoFrames),
droppedFrames: Number(videoInfo.droppedVideoFrames),
estimatedBandwidth: this.config_.abr.manager.getBandwidthEstimate(),
playTime: this.playTime_,
bufferingTime: this.bufferingTime_,
switchHistory: this.switchHistory_.slice(0)
};
};
/**
* Adds the given text track to the current Period. Load() must resolve before
* calling. The current Period or the presentation must have a duration. This
* returns a Promise that will resolve when the track can be switched to and
* will resolve with the track that was created.
*
* @param {string} uri
* @param {string} language
* @param {string} kind
* @param {string} mime
* @param {string=} opt_codec
* @return {!Promise.<shakaExtern.Track>}
* @export
*/
shaka.Player.prototype.addTextTrack = function(
uri, language, kind, mime, opt_codec) {
if (!this.streamingEngine_) {
shaka.log.error(
'Must call load() and wait for it to resolve before adding text ' +
'tracks.');
return Promise.reject();
}
// Get the Period duration.
var period = this.streamingEngine_.getCurrentPeriod();
/** @type {number} */
var periodDuration;
for (var i = 0; i < this.manifest_.periods.length; i++) {
if (this.manifest_.periods[i] == period) {
if (i == this.manifest_.periods.length - 1) {
periodDuration = this.manifest_.presentationTimeline.getDuration() -
period.startTime;
if (periodDuration == Infinity) {
shaka.log.error(
'The current Period or the presentation must have a duration ' +
'to add external text tracks.');
return Promise.reject();
}
} else {
var nextPeriod = this.manifest_.periods[i + 1];
periodDuration = nextPeriod.startTime - period.startTime;
}
break;
}
}
/** @type {shakaExtern.Stream} */
var stream = {
id: this.nextExternalStreamId_++,
createSegmentIndex: Promise.resolve.bind(Promise),
findSegmentPosition: function(time) { return 1; },
getSegmentReference: function(ref) {
if (ref != 1) return null;
return new shaka.media.SegmentReference(
1, 0, periodDuration, function() { return [uri]; }, 0, null);
},
initSegmentReference: null,
presentationTimeOffset: 0,
mimeType: mime,
codecs: opt_codec || '',
bandwidth: 0,
kind: kind,
encrypted: false,
keyId: null,
language: language,
allowedByApplication: true,
allowedByKeySystem: true
};
/** @type {shakaExtern.StreamSet} */
var streamSet = {
language: language,
type: 'text',
primary: false,
drmInfos: [],
streams: [stream]
};
// Add the stream to the loading list to ensure it isn't switched to while it
// is initializing.
this.loadingTextStreamIds_.push(stream.id);
period.streamSets.push(streamSet);
return this.streamingEngine_.notifyNewStream('text', stream).then(function() {
if (this.destroyed_) return;
// Remove the stream from the loading list.
this.loadingTextStreamIds_.splice(
this.loadingTextStreamIds_.indexOf(stream.id), 1);
shaka.log.debug('Choosing new streams after adding a text stream');
this.chooseStreamsAndSwitch_(period);
this.onTracksChanged_();
return {
id: stream.id,
active: false,
type: 'text',
bandwidth: 0,
language: language,
kind: kind,
width: null,
height: null
};
}.bind(this));
};
/**
* Set the maximum resolution that the platform's hardware can handle.
* This will be called automatically by shaka.cast.CastReceiver to enforce
* limitations of the Chromecast hardware.
*
* @param {number} width
* @param {number} height
* @export
*/
shaka.Player.prototype.setMaxHardwareResolution = function(width, height) {
this.maxHwRes_.width = width;
this.maxHwRes_.height = height;
};
/**
* Initialize the Player.
* @private
*/
shaka.Player.prototype.initialize_ = function() {
// Start the (potentially slow) process of opening MediaSource now.
this.mediaSourceOpen_ = this.createMediaSource();
// If the video element has TextTracks, disable them. If we see one that
// was created by a previous instance of Shaka Player, reuse it.
for (var i = 0; i < this.video_.textTracks.length; ++i) {
var track = this.video_.textTracks[i];
track.mode = 'disabled';
if (track.label == shaka.Player.TextTrackLabel_) {
this.textTrack_ = track;
}
}
if (!this.textTrack_) {
// As far as I can tell, there is no observable difference between setting
// kind to 'subtitles' or 'captions' when creating the TextTrack object.
// The individual text tracks from the manifest will still have their own
// kinds which can be displayed in the app's UI.
this.textTrack_ = this.video_.addTextTrack(
'subtitles', shaka.Player.TextTrackLabel_);
}
this.textTrack_.mode = 'hidden';
// TODO: test that in all cases, the built-in CC controls in the video element
// are toggling our TextTrack.
// Listen for video errors.
this.eventManager_.listen(this.video_, 'error',
this.onVideoError_.bind(this));
};
/**
* Destroy members responsible for streaming.
*
* @return {!Promise}
* @private
*/
shaka.Player.prototype.destroyStreaming_ = function() {
if (this.eventManager_) {
this.eventManager_.unlisten(this.mediaSource_, 'sourceopen');
}
if (this.video_) {
this.video_.removeAttribute('src');
this.video_.load();
}
var p = Promise.all([
this.config_ ? this.config_.abr.manager.stop() : null,
this.drmEngine_ ? this.drmEngine_.destroy() : null,
this.mediaSourceEngine_ ? this.mediaSourceEngine_.destroy() : null,
this.playhead_ ? this.playhead_.destroy() : null,
this.streamingEngine_ ? this.streamingEngine_.destroy() : null,
this.parser_ ? this.parser_.stop() : null
]);
this.drmEngine_ = null;
this.mediaSourceEngine_ = null;
this.playhead_ = null;
this.streamingEngine_ = null;
this.parser_ = null;
this.manifest_ = null;
this.manifestUri_ = null;
this.mediaSourceOpen_ = null;
this.mediaSource_ = null;
this.deferredSwitches_ = {};
this.switchHistory_ = [];
this.playTime_ = 0;
this.bufferingTime_ = 0;
return p;
};
/**
* Reset the streaming system.
* @return {!Promise}
* @private
*/
shaka.Player.prototype.resetStreaming_ = function() {
if (!this.parser_) {
// Nothing is playing, so this is effectively a no-op.
return Promise.resolve();
}
// Destroy the streaming system before we recreate everything.
return this.destroyStreaming_().then(function() {
if (this.destroyed_) return;
// Force an exit from the buffering state.
this.onBuffering_(false);
// Start the (potentially slow) process of opening MediaSource now.
this.mediaSourceOpen_ = this.createMediaSource();
}.bind(this));
};
/**
* @const {string}
* @private
*/
shaka.Player.TextTrackLabel_ = 'Shaka Player TextTrack';
/**
* @return {!Object}
* @private
*/
shaka.Player.prototype.configOverrides_ = function() {
return {
'.drm.servers': '',
'.drm.clearKeys': '',
'.drm.advanced': {
distinctiveIdentifierRequired: false,
persistentStateRequired: false,
videoRobustness: '',
audioRobustness: '',
serverCertificate: null
}
};
};
/**
* @return {shakaExtern.PlayerConfiguration}
* @private
*/
shaka.Player.prototype.defaultConfig_ = function() {
return {
drm: {
retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
// These will all be verified by special cases in mergeConfigObjects_():
servers: {}, // key is arbitrary key system ID, value must be string
clearKeys: {}, // key is arbitrary key system ID, value must be string
advanced: {} // key is arbitrary key system ID, value is a record type
},
manifest: {
retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
dash: {
customScheme: function(node) {
// Reference node to keep closure from removing it.
// If the argument is removed, it breaks our function length check
// in mergeConfigObjects_().
// TODO: Find a better solution if possible.
// NOTE: Chrome App Content Security Policy prohibits usage of new
// Function()
if (node) return null;
},
clockSyncUri: ''
}
},
streaming: {
retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
rebufferingGoal: 2,
bufferingGoal: 10,
bufferBehind: 30,
ignoreTextStreamFailures: false,
useRelativeCueTimestamps: false
},
abr: {
manager: this.defaultAbrManager_,
enabled: true,
defaultBandwidthEstimate:
shaka.abr.EwmaBandwidthEstimator.DEFAULT_ESTIMATE
},
preferredAudioLanguage: '',
preferredTextLanguage: '',
restrictions: {
minWidth: 0,
maxWidth: Infinity,
minHeight: 0,
maxHeight: Infinity,
minPixels: 0,
maxPixels: Infinity,
minAudioBandwidth: 0,
maxAudioBandwidth: Infinity,
minVideoBandwidth: 0,
maxVideoBandwidth: Infinity
}
};
};
/**
* @param {shakaExtern.Period} period
* @private
*/
shaka.Player.prototype.filterPeriod_ = function(period) {
goog.asserts.assert(this.video_, 'Must not be destroyed');
var activeStreams =
this.streamingEngine_ ? this.streamingEngine_.getActiveStreams() : {};
shaka.util.StreamUtils.filterPeriod(this.drmEngine_, activeStreams, period);
// Check for playable streams before restrictions to give a different error
// if we have restricted all the tracks rather than there being none.
var hasPlayableStreamSets =
period.streamSets.some(shaka.util.StreamUtils.hasPlayableStreams);
var tracksChanged = shaka.util.StreamUtils.applyRestrictions(
period, this.config_.restrictions, this.maxHwRes_);
if (tracksChanged && !this.loadChain_)
this.onTracksChanged_();
var allStreamsRestricted =
!period.streamSets.some(shaka.util.StreamUtils.hasPlayableStreams);
if (!hasPlayableStreamSets) {
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.UNPLAYABLE_PERIOD);
} else if (allStreamsRestricted) {
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET);
}
};
/**
* Switches to the given streams, deferring switches if needed.
* @param {!Object.<string, shakaExtern.Stream>} streamsByType
* @param {boolean=} opt_clearBuffer
* @private
*/
shaka.Player.prototype.deferredSwitch_ = function(
streamsByType, opt_clearBuffer) {
for (var type in streamsByType) {
var stream = streamsByType[type];
var clearBuffer = opt_clearBuffer || false;
// TODO: consider adding a cue replacement algorithm to TextEngine to remove
// this special case for text:
if (type == 'text') clearBuffer = true;
if (this.switchingPeriods_) {
this.deferredSwitches_[type] = {stream: stream, clearBuffer: clearBuffer};
} else {
this.streamingEngine_.switch(type, stream, clearBuffer);
}
}
};
/** @private */
shaka.Player.prototype.updateStats_ = function() {
// Only count while we're loaded.
if (!this.manifest_)
return;
var now = Date.now() / 1000;
if (this.buffering_)
this.bufferingTime_ += (now - this.lastStatUpdateTimestamp_);
else
this.playTime_ += (now - this.lastStatUpdateTimestamp_);
this.lastStatUpdateTimestamp_ = now;
};
/**
* Callback from NetworkingEngine.
*
* @param {number} startTimeMs
* @param {number} endTimeMs
* @param {number} numBytes
* @private
*/
shaka.Player.prototype.onSegmentDownloaded_ = function(
startTimeMs, endTimeMs, numBytes) {
this.config_.abr.manager.segmentDownloaded(startTimeMs, endTimeMs, numBytes);
};
/**
* Callback from Playhead.
*
* @param {boolean} buffering
* @private
*/
shaka.Player.prototype.onBuffering_ = function(buffering) {
// Before setting |buffering_|, update the time spent in the previous state.
this.updateStats_();
this.buffering_ = buffering;
var event = new shaka.util.FakeEvent('buffering', { 'buffering': buffering });
this.dispatchEvent(event);
};
/**
* Callback from Playhead.
*
* @private
*/
shaka.Player.prototype.onSeek_ = function() {
if (this.streamingEngine_) {
this.streamingEngine_.seeked();
}
};
/**
* Chooses streams from the given Period.
*
* @param {!shakaExtern.Period} period
* @param {!Object.<string, shakaExtern.StreamSet>} streamSetsByType
* @param {boolean=} opt_chooseAll If true, choose streams from all stream sets.
* @return {!Object.<string, !shakaExtern.Stream>} A map of stream types to
* chosen streams.
* @private
*/
shaka.Player.prototype.chooseStreams_ =
function(period, streamSetsByType, opt_chooseAll) {
goog.asserts.assert(this.config_, 'Must not be destroyed');
var hasPlayableStreams = shaka.util.StreamUtils.hasPlayableStreams;
// If there are no unrestricted streams, issue an error.
var manifestIsPlayable =
shaka.util.MapUtils.values(streamSetsByType).some(hasPlayableStreams);
if (!manifestIsPlayable) {
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET));
return {};
}
// Which StreamSets need to have their chosen stream updated.
var needsUpdate = {};
if (opt_chooseAll) {
// Choose new streams for all recognized types.
// When MIME types starting with 'application/' were incorrectly turned
// into StreamSets of type 'application', we fed these to AbrManager.
// AbrManager ignored them, and this caused us to fail with a very
// confusing RESTRICTIONS_CANNOT_BE_MET error.
// It is important that we only feed AbrManager things we can expect it to
// deal with. That way, we can later check that AbrManager chose streams
// for all the types we requested.
var recognizedTypes = ['video', 'audio', 'text'];
recognizedTypes.forEach(function(type) {
if (type in streamSetsByType) {
needsUpdate[type] = streamSetsByType[type];
}
});
} else {
// Check if any of the active streams is no longer available
// or is using the wrong language.
var activeStreams = this.streamingEngine_.getActiveStreams();
for (var type in activeStreams) {
var stream = activeStreams[type];
if (!stream.allowedByApplication ||
!stream.allowedByKeySystem ||
streamSetsByType[type].language != stream.language) {
needsUpdate[type] = streamSetsByType[type];
}
}
}
if (!shaka.util.MapUtils.empty(needsUpdate)) {
shaka.log.debug('Choosing new streams for', Object.keys(needsUpdate));
var chosen = this.config_.abr.manager.chooseStreams(needsUpdate);
if (!shaka.util.MapUtils.every(needsUpdate,
function(type, set) { return !!chosen[type]; })) {
// Not every type was chosen! This should only happen when output
// restrictions prevent playback of a certain type and there are no
// usable streams for that type.
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET));
return {};
}
return chosen;
} else {
shaka.log.debug('No new streams need to be chosen.');
return {};
}
};
/**
* Chooses streams from the given Period and switches to them.
* Called after a config change, a new text stream, or a key status event.
*
* @param {!shakaExtern.Period} period
* @private
*/
shaka.Player.prototype.chooseStreamsAndSwitch_ = function(period) {
goog.asserts.assert(this.config_, 'Must not be destroyed');
var languageMatches = { 'audio': false, 'text': false };
var streamSetsByType = shaka.util.StreamUtils.chooseStreamSets(
period, this.config_, languageMatches);
// chooseStreams_ filters out choices which are already active.
var chosen = this.chooseStreams_(period, streamSetsByType);
for (var type in chosen) {
var stream = chosen[type];
this.switchHistory_.push({
timestamp: Date.now() / 1000,
id: stream.id,
type: type,
fromAdaptation: true
});
}
// Because we're running this after a config change (manual language change),
// a new text stream, or a key status event, and because active streams have
// been filtered out already, it is always okay to clear the buffer for what
// remains.
this.deferredSwitch_(chosen, true);
// Send an adaptation event so that the UI can show the new language/tracks.
this.onAdaptation_();
if (streamSetsByType['text']) {
// If audio and text tracks have different languages, and the text track
// matches the user's preference, then show the captions.
if (streamSetsByType['audio'] &&
languageMatches['text'] &&
streamSetsByType['text'].language !=
streamSetsByType['audio'].language) {
this.textTrack_.mode = 'showing';
this.onTextTrackVisibility_();
}
}
};
/**
* Callback from StreamingEngine.
*
* @param {!shakaExtern.Period} period
* @return {!Object.<string, !shakaExtern.Stream>} A map of stream types to
* chosen streams.
* @private
*/
shaka.Player.prototype.onChooseStreams_ = function(period) {
shaka.log.debug('onChooseStreams_', period);
goog.asserts.assert(this.config_, 'Must not be destroyed');
// We are switching Periods, so the AbrManager will be disabled. But if we
// want to abr.enabled, we do not want to call AbrManager.enable before
// canSwitch_ is called.
this.switchingPeriods_ = true;
this.config_.abr.manager.disable();
shaka.log.debug('Choosing new streams after period changed');
var streamSetsByType = shaka.util.StreamUtils.chooseStreamSets(
period, this.config_);
shaka.log.v2('onChooseStreams_, streamSetsByType=', streamSetsByType);
var chosen = this.chooseStreams_(
period, streamSetsByType, /* opt_chooseAll */ true);
shaka.log.v2('onChooseStreams_, chosen=', chosen);
// Override the chosen streams with the ones picked in selectTrack.
// NOTE: The apparent race between selectTrack and period transition is
// handled by StreamingEngine, which will re-request tracks for the
// transition if any of these deferred selections are from the wrong period.
for (var type in this.deferredSwitches_) {
// We are choosing initial tracks, so no segments from this Period have
// been downloaded yet. Therefore, it is okay to ignore the .clearBuffer
// member of this structure.
chosen[type] = this.deferredSwitches_[type].stream;
}
this.deferredSwitches_ = {};
for (var type in chosen) {
var stream = chosen[type];
this.switchHistory_.push({
timestamp: Date.now() / 1000,
id: stream.id,
type: type,
fromAdaptation: true
});
}
// If we are presently loading, we aren't done filtering streams just yet.
// Wait to send a 'trackschanged' event.
if (!this.loadChain_) {
this.onTracksChanged_();
}
return chosen;
};
/**
* Callback from StreamingEngine.
*
* @private
*/
shaka.Player.prototype.canSwitch_ = function() {
shaka.log.debug('canSwitch_');
this.switchingPeriods_ = false;
if (this.config_.abr.enabled)
this.config_.abr.manager.enable();
// If we still have deferred switches, switch now.
for (var type in this.deferredSwitches_) {
var info = this.deferredSwitches_[type];
this.streamingEngine_.switch(type, info.stream, info.clearBuffer);
}
this.deferredSwitches_ = {};
};
/**
* Callback from AbrManager.
*
* @param {!Object.<string, !shakaExtern.Stream>} streamsByType
* @param {boolean=} opt_clearBuffer
* @private
*/
shaka.Player.prototype.switch_ = function(streamsByType, opt_clearBuffer) {
shaka.log.debug('switch_');
goog.asserts.assert(this.config_.abr.enabled,
'AbrManager should not call switch while disabled!');
goog.asserts.assert(!this.switchingPeriods_,
'AbrManager should not call switch while transitioning between Periods!');
// We have adapted to a new stream, record it in the history. Only add if
// we are actually switching the stream.
var oldActive = this.streamingEngine_.getActiveStreams();
for (var type in streamsByType) {
var stream = streamsByType[type];
if (oldActive[type] != stream) {
this.switchHistory_.push({
timestamp: Date.now() / 1000,
id: stream.id,
type: type,
fromAdaptation: true
});
} else {
// If it's the same, remove it from the map.
// This allows us to avoid onAdaptation_() when nothing has changed.
delete streamsByType[type];
}
}
if (shaka.util.MapUtils.empty(streamsByType)) {
// There's nothing to change.
return;
}
if (!this.streamingEngine_) {
// There's no way to change it.
return;
}
for (var type in streamsByType) {
var clearBuffer = opt_clearBuffer || false;
this.streamingEngine_.switch(type, streamsByType[type], clearBuffer);
}
this.onAdaptation_();
};
/**
* Dispatches a 'adaptation' event.
* @private
*/
shaka.Player.prototype.onAdaptation_ = function() {
// In the next frame, dispatch a 'adaptation' event.
// This gives StreamingEngine time to absorb the changes before the user
// tries to query them.
Promise.resolve().then(function() {
if (this.destroyed_) return;
var event = new shaka.util.FakeEvent('adaptation');
this.dispatchEvent(event);
}.bind(this));
};
/**
* Dispatches a 'trackschanged' event.
* @private
*/
shaka.Player.prototype.onTracksChanged_ = function() {
// In the next frame, dispatch a 'trackschanged' event.
// This gives StreamingEngine time to absorb the changes before the user
// tries to query them.
Promise.resolve().then(function() {
if (this.destroyed_) return;
var event = new shaka.util.FakeEvent('trackschanged');
this.dispatchEvent(event);
}.bind(this));
};
/** @private */
shaka.Player.prototype.onTextTrackVisibility_ = function() {
var event = new shaka.util.FakeEvent('texttrackvisibility');
this.dispatchEvent(event);
};
/**
* @param {!shaka.util.Error} error
* @private
*/
shaka.Player.prototype.onError_ = function(error) {
goog.asserts.assert(error instanceof shaka.util.Error, 'Wrong error type!');
var event = new shaka.util.FakeEvent('error', { 'detail': error });
this.dispatchEvent(event);
};
/**
* @param {!Event} event
* @private
*/
shaka.Player.prototype.onEvent_ = function(event) {
this.dispatchEvent(event);
};
/**
* @param {!Event} event
* @private
*/
shaka.Player.prototype.onVideoError_ = function(event) {
if (!this.video_.error) return;
var code = this.video_.error.code;
if (code == 1 /* MEDIA_ERR_ABORTED */) {
// Ignore this error code, which should only occur when navigating away or
// deliberately stopping playback of HTTP content.
return;
}
// Extra error information from MS Edge and IE11:
var extended = this.video_.error.msExtendedCode;
if (extended) {
// Convert to unsigned:
if (extended < 0) {
extended += Math.pow(2, 32);
}
// Format as hex:
extended = extended.toString(16);
}
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.VIDEO_ERROR,
code, extended));
};
/**
* @param {!Object.<string, string>} keyStatusMap A map of hex key IDs to
* statuses.
* @private
*/
shaka.Player.prototype.onKeyStatus_ = function(keyStatusMap) {
goog.asserts.assert(this.streamingEngine_, 'Should have been initialized.');
// 'usable', 'released', 'output-downscaled', 'status-pending' are statuses
// of the usable keys.
// 'expired' status is being handled separately in DrmEngine.
var restrictedStatuses = ['output-restricted', 'internal-error'];
var period = this.streamingEngine_.getCurrentPeriod();
var tracksChanged = false;
period.streamSets.forEach(function(streamSet) {
streamSet.streams.forEach(function(stream) {
var originalAllowed = stream.allowedByKeySystem;
// Only update the status if it is in the map.
if (stream.keyId && stream.keyId in keyStatusMap) {
var keyStatus = keyStatusMap[stream.keyId];
stream.allowedByKeySystem = restrictedStatuses.indexOf(keyStatus) < 0;
}
if (originalAllowed != stream.allowedByKeySystem) {
tracksChanged = true;
}
});
});
shaka.log.debug('Choosing new streams after key status changed');
this.chooseStreamsAndSwitch_(period);
if (tracksChanged)
this.onTracksChanged_();
};
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| ewma.js | 6.25% | (1 / 16) | 0% | (0 / 2) | 0% | (0 / 3) | 6.25% | (1 / 16) | |
| ewma_bandwidth_estimator.js | 3.85% | (1 / 26) | 0% | (0 / 4) | 0% | (0 / 5) | 3.85% | (1 / 26) | |
| simple_abr_manager.js | 0.85% | (1 / 118) | 0% | (0 / 41) | 0% | (0 / 16) | 0.85% | (1 / 117) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.abr.Ewma');
goog.require('goog.asserts');
/**
* Computes an exponentionally-weighted moving average.
*
* @param {number} halfLife About half of the estimated value will be from the
* last |halfLife| samples by weight.
* @struct
* @constructor
*/
shaka.abr.Ewma = function(halfLife) {
goog.asserts.assert(halfLife > 0, 'expected halfLife to be positive');
/**
* Larger values of alpha expire historical data more slowly.
* @private {number}
*/
this.alpha_ = Math.exp(Math.log(0.5) / halfLife);
/** @private {number} */
this.estimate_ = 0;
/** @private {number} */
this.totalWeight_ = 0;
};
/**
* Takes a sample.
*
* @param {number} weight
* @param {number} value
*/
shaka.abr.Ewma.prototype.sample = function(weight, value) {
var adjAlpha = Math.pow(this.alpha_, weight);
var newEstimate = value * (1 - adjAlpha) + adjAlpha * this.estimate_;
if (!isNaN(newEstimate)) {
this.estimate_ = newEstimate;
this.totalWeight_ += weight;
}
};
/**
* @return {number}
*/
shaka.abr.Ewma.prototype.getEstimate = function() {
var zeroFactor = 1 - Math.pow(this.alpha_, this.totalWeight_);
return this.estimate_ / zeroFactor;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.abr.EwmaBandwidthEstimator');
goog.require('shaka.abr.Ewma');
/**
* Tracks bandwidth samples and estimates available bandwidth.
* Based on the minimum of two exponentially-weighted moving averages with
* different half-lives.
*
* @constructor
* @struct
*/
shaka.abr.EwmaBandwidthEstimator = function() {
/**
* A fast-moving average.
* Half of the estimate is based on the last 2 seconds of sample history.
* @private {!shaka.abr.Ewma}
*/
this.fast_ = new shaka.abr.Ewma(2);
/**
* A slow-moving average.
* Half of the estimate is based on the last 5 seconds of sample history.
* @private {!shaka.abr.Ewma}
*/
this.slow_ = new shaka.abr.Ewma(5);
/**
* Number of bytes sampled.
* @private {number}
*/
this.bytesSampled_ = 0;
/**
* Initial estimate used when there is not enough data.
* @see shaka.abr.EwmaBandwidthEstimator.DEFAULT_ESTIMATE
* @private {number}
*/
this.defaultEstimate_ = shaka.abr.EwmaBandwidthEstimator.DEFAULT_ESTIMATE;
/**
* Minimum number of bytes sampled before we trust the estimate. If we have
* not sampled much data, our estimate may not be accurate enough to trust.
* If bytesSampled_ is less than minTotalBytes_, we use defaultEstimate_.
* This specific value is based on experimentation.
*
* @private {number}
* @const
*/
this.minTotalBytes_ = 128e3; // 128kB
/**
* Minimum number of bytes, under which samples are discarded. Our models do
* not include latency information, so connection startup time (time to first
* byte) is considered part of the download time. Because of this, we should
* ignore very small downloads which would cause our estimate to be too low.
* This specific value is based on experimentation.
*
* @private {number}
* @const
*/
this.minBytes_ = 16e3; // 16kB
};
/**
* Contains the default estimate to use when there is not enough data.
* This is a relatively safe default, since 3G cell connections are faster than
* this. For slower connections, such as 2G, the default estimate may be too
* high. This default can be changed at runtime using
* {@link shaka.Player#configure} and {@link shakaExtern.AbrConfiguration}.
* @const {number}
*/
shaka.abr.EwmaBandwidthEstimator.DEFAULT_ESTIMATE = 500e3; // 500kbps
/**
* Takes a bandwidth sample.
*
* @param {number} durationMs The amount of time, in milliseconds, for a
* particular request.
* @param {number} numBytes The total number of bytes transferred in that
* request.
*/
shaka.abr.EwmaBandwidthEstimator.prototype.sample = function(
durationMs, numBytes) {
if (numBytes < this.minBytes_) {
return;
}
var bandwidth = 8000 * numBytes / durationMs;
var weight = durationMs / 1000;
this.bytesSampled_ += numBytes;
this.fast_.sample(weight, bandwidth);
this.slow_.sample(weight, bandwidth);
};
/**
* Sets the default bandwidth estimate to use if there is not enough data.
*
* @param {number} estimate The default bandwidth estimate, in bit/sec.
*/
shaka.abr.EwmaBandwidthEstimator.prototype.setDefaultEstimate = function(
estimate) {
this.defaultEstimate_ = estimate;
};
/**
* Gets the current bandwidth estimate.
*
* @return {number} The bandwidth estimate in bits per second.
*/
shaka.abr.EwmaBandwidthEstimator.prototype.getBandwidthEstimate = function() {
if (this.bytesSampled_ < this.minTotalBytes_) {
return this.defaultEstimate_;
}
// Take the minimum of these two estimates. This should have the effect of
// adapting down quickly, but up more slowly.
return Math.min(this.fast_.getEstimate(), this.slow_.getEstimate());
};
/**
* @return {boolean} True if there is enough data to produce a meaningful
* estimate.
*/
shaka.abr.EwmaBandwidthEstimator.prototype.hasGoodEstimate = function() {
return this.bytesSampled_ >= this.minTotalBytes_;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.abr.SimpleAbrManager');
goog.require('goog.asserts');
goog.require('shaka.abr.EwmaBandwidthEstimator');
goog.require('shaka.log');
/**
* Creates a new SimpleAbrManager.
*
* @constructor
* @struct
* @implements {shakaExtern.AbrManager}
* @export
*/
shaka.abr.SimpleAbrManager = function() {
/** @private {?shakaExtern.AbrManager.SwitchCallback} */
this.switch_ = null;
/** @private {boolean} */
this.enabled_ = false;
/** @private {shaka.abr.EwmaBandwidthEstimator} */
this.bandwidthEstimator_ = new shaka.abr.EwmaBandwidthEstimator();
/**
* The last StreamSets given to us via chooseStreams().
* @private {Object.<string, shakaExtern.StreamSet>}
*/
this.streamSetsByType_ = {};
/**
* The last Streams chosen.
* @private {Object.<string, shakaExtern.Stream>}
*/
this.streamsByType_ = {};
/** @private {boolean} */
this.startupComplete_ = false;
/**
* The last wall-clock time, in milliseconds, when Streams were chosen via
* chooseStreams() or switch_().
*
* @private {?number}
*/
this.lastTimeChosenMs_ = null;
};
/**
* The minimum amount of time that must pass between switches, in milliseconds.
* This keeps us from changing too often and annoying the user.
*
* @const {number}
*/
shaka.abr.SimpleAbrManager.SWITCH_INTERVAL_MS = 8000;
/**
* The fraction of the estimated bandwidth which we should try to use when
* upgrading.
*
* @private
* @const {number}
*/
shaka.abr.SimpleAbrManager.BANDWIDTH_UPGRADE_TARGET_ = 0.85;
/**
* The largest fraction of the estimated bandwidth we should use. We should
* downgrade to avoid this.
*
* @private
* @const {number}
*/
shaka.abr.SimpleAbrManager.BANDWIDTH_DOWNGRADE_TARGET_ = 0.95;
/**
* @override
* @export
*/
shaka.abr.SimpleAbrManager.prototype.stop = function() {
this.switch_ = null;
this.enabled_ = false;
this.streamSetsByType_ = {};
this.streamsByType_ = {};
this.lastTimeChosenMs_ = null;
// Don't reset |startupComplete_|: if we've left the startup interval then we
// can start using bandwidth estimates right away if init() is called again.
};
/**
* @override
* @export
*/
shaka.abr.SimpleAbrManager.prototype.init = function(switchCallback) {
this.switch_ = switchCallback;
};
/**
* @override
* @export
*/
shaka.abr.SimpleAbrManager.prototype.chooseStreams = function(
streamSetsByType) {
// Merge StreamSets. We may have been given a partial list.
for (var type in streamSetsByType) {
this.streamSetsByType_[type] = streamSetsByType[type];
}
// Choose streams for the specific types requested.
var chosen = {};
if ('audio' in streamSetsByType) {
var audioStream = this.chooseAudioStream_();
if (audioStream) {
chosen['audio'] = audioStream;
this.streamsByType_['audio'] = audioStream;
} else {
delete this.streamsByType_['audio'];
}
}
if ('video' in streamSetsByType) {
var videoStream = this.chooseVideoStream_();
if (videoStream) {
chosen['video'] = videoStream;
this.streamsByType_['video'] = videoStream;
} else {
delete this.streamsByType_['video'];
}
}
if ('text' in streamSetsByType) {
// We don't adapt text, so just choose stream 0.
chosen['text'] = streamSetsByType['text'].streams[0];
}
this.lastTimeChosenMs_ = Date.now();
return chosen;
};
/**
* @override
* @export
*/
shaka.abr.SimpleAbrManager.prototype.enable = function() {
this.enabled_ = true;
};
/**
* @override
* @export
*/
shaka.abr.SimpleAbrManager.prototype.disable = function() {
this.enabled_ = false;
};
/**
* @override
* @export
*/
shaka.abr.SimpleAbrManager.prototype.segmentDownloaded = function(
startTimeMs, endTimeMs, numBytes) {
shaka.log.v2('Segment downloaded:',
'startTimeMs=' + startTimeMs,
'endTimeMs=' + endTimeMs,
'numBytes=' + numBytes);
goog.asserts.assert(endTimeMs >= startTimeMs,
'expected a non-negative duration');
this.bandwidthEstimator_.sample(endTimeMs - startTimeMs, numBytes);
if ((this.lastTimeChosenMs_ != null) && this.enabled_)
this.suggestStreams_();
};
/**
* @override
* @export
*/
shaka.abr.SimpleAbrManager.prototype.getBandwidthEstimate = function() {
return this.bandwidthEstimator_.getBandwidthEstimate();
};
/**
* @override
* @export
*/
shaka.abr.SimpleAbrManager.prototype.setDefaultEstimate = function(estimate) {
this.bandwidthEstimator_.setDefaultEstimate(estimate);
};
/**
* Calls switch_() with which Streams to switch to.
*
* @private
*/
shaka.abr.SimpleAbrManager.prototype.suggestStreams_ = function() {
shaka.log.v2('Suggesting Streams...');
goog.asserts.assert(this.lastTimeChosenMs_ != null,
'lastTimeChosenMs_ should not be null');
if (!this.startupComplete_) {
// Check if we've got enough data yet.
if (!this.bandwidthEstimator_.hasGoodEstimate()) {
shaka.log.v2('Still waiting for a good estimate...');
return;
}
this.startupComplete_ = true;
} else {
// Check if we've left the switch interval.
var now = Date.now();
var delta = now - this.lastTimeChosenMs_;
if (delta < shaka.abr.SimpleAbrManager.SWITCH_INTERVAL_MS) {
shaka.log.v2('Still within switch interval...');
return;
}
}
var chosen = this.chooseStreams_();
var currentBandwidthKbps =
Math.round(this.bandwidthEstimator_.getBandwidthEstimate() / 1000.0);
shaka.log.debug(
'Calling switch_(), bandwidth=' + currentBandwidthKbps + ' kbps');
// If any of these chosen streams are already chosen, Player will filter them
// out before passing the choices on to StreamingEngine.
this.switch_(chosen);
};
/**
* Chooses which Streams to switch to.
*
* @return {!Object.<string, !shakaExtern.Stream>}
* @private
*/
shaka.abr.SimpleAbrManager.prototype.chooseStreams_ = function() {
var streamsByType = {};
// Choose audio Stream.
var audioStream = this.chooseAudioStream_();
if (audioStream) {
streamsByType['audio'] = audioStream;
this.streamsByType_['audio'] = audioStream;
}
// Choose video Stream.
var videoStream = this.chooseVideoStream_();
if (videoStream) {
streamsByType['video'] = videoStream;
this.streamsByType_['video'] = videoStream;
}
this.lastTimeChosenMs_ = Date.now();
return streamsByType;
};
/**
* Chooses which audio Stream to switch to.
*
* @return {?shakaExtern.Stream}
* @private
*/
shaka.abr.SimpleAbrManager.prototype.chooseAudioStream_ = function() {
// Alias.
var SimpleAbrManager = shaka.abr.SimpleAbrManager;
// Get sorted audio Streams.
var audioStreamSet = this.streamSetsByType_['audio'];
if (!audioStreamSet)
return null;
var audioStreams = SimpleAbrManager.sortStreamsByBandwidth_(audioStreamSet);
// Just pick the middle one.
// TODO: Implement better audio adaptation.
return audioStreams[Math.floor(audioStreams.length / 2)];
};
/**
* Chooses which video Stream to switch to.
*
* @return {?shakaExtern.Stream}
* @private
*/
shaka.abr.SimpleAbrManager.prototype.chooseVideoStream_ = function() {
// Alias.
var SimpleAbrManager = shaka.abr.SimpleAbrManager;
// Get sorted video Streams.
var videoStreamSet = this.streamSetsByType_['video'];
if (!videoStreamSet)
return null;
var videoStreams = SimpleAbrManager.sortStreamsByBandwidth_(videoStreamSet);
var audioStream = this.streamsByType_['audio'];
var audioBandwidth = (audioStream && audioStream.bandwidth) || 0;
var currentBandwidth = this.bandwidthEstimator_.getBandwidthEstimate();
// Start by assuming that we will use the first Stream.
var chosen = videoStreams[0];
for (var i = 0; i < videoStreams.length; ++i) {
var stream = videoStreams[i];
var nextStream = (i + 1 < videoStreams.length) ?
videoStreams[i + 1] :
{bandwidth: Infinity};
// Ignore Streams which don't have bandwidth information.
if (!stream.bandwidth) continue;
var minBandwidth = (stream.bandwidth + audioBandwidth) /
SimpleAbrManager.BANDWIDTH_DOWNGRADE_TARGET_;
var maxBandwidth = (nextStream.bandwidth + audioBandwidth) /
SimpleAbrManager.BANDWIDTH_UPGRADE_TARGET_;
shaka.log.v2('Bandwidth ranges:',
((stream.bandwidth + audioBandwidth) / 1e6).toFixed(3),
(minBandwidth / 1e6).toFixed(3),
(maxBandwidth / 1e6).toFixed(3));
if (currentBandwidth >= minBandwidth && currentBandwidth <= maxBandwidth)
chosen = stream;
}
return chosen;
};
/**
* @param {!shakaExtern.StreamSet} streamSet
* @return {!Array.<shakaExtern.Stream>} |streamSet|'s Streams sorted
* in ascending order of bandwidth.
* @private
*/
shaka.abr.SimpleAbrManager.sortStreamsByBandwidth_ = function(streamSet) {
return streamSet.streams.slice(0)
.filter(function(s) {
return s.allowedByApplication && s.allowedByKeySystem;
})
.sort(function(s1, s2) { return s1.bandwidth - s2.bandwidth; });
};
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| cast_proxy.js | 0.56% | (1 / 177) | 0% | (0 / 64) | 0% | (0 / 32) | 0.56% | (1 / 177) | |
| cast_receiver.js | 0.57% | (1 / 175) | 0% | (0 / 43) | 0% | (0 / 28) | 0.57% | (1 / 174) | |
| cast_sender.js | 0.52% | (1 / 194) | 0% | (0 / 61) | 0% | (0 / 31) | 0.52% | (1 / 193) | |
| cast_utils.js | 1.67% | (1 / 60) | 0% | (0 / 35) | 0% | (0 / 8) | 1.75% | (1 / 57) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.cast.CastProxy');
goog.require('goog.asserts');
goog.require('shaka.cast.CastSender');
goog.require('shaka.cast.CastUtils');
goog.require('shaka.log');
goog.require('shaka.util.Error');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.IDestroyable');
/**
* A proxy to switch between local and remote playback for Chromecast in a way
* that is transparent to the app's controls.
*
* @constructor
* @struct
* @param {!HTMLMediaElement} video The local video element associated with the
* local Player instance.
* @param {!shaka.Player} player A local Player instance.
* @param {string} receiverAppId The ID of the cast receiver application.
* @implements {shaka.util.IDestroyable}
* @extends {shaka.util.FakeEventTarget}
* @export
*/
shaka.cast.CastProxy = function(video, player, receiverAppId) {
shaka.util.FakeEventTarget.call(this);
/** @private {HTMLMediaElement} */
this.localVideo_ = video;
/** @private {shaka.Player} */
this.localPlayer_ = player;
/** @private {Object} */
this.videoProxy_ = null;
/** @private {Object} */
this.playerProxy_ = null;
/** @private {shaka.util.FakeEventTarget} */
this.videoEventTarget_ = null;
/** @private {shaka.util.FakeEventTarget} */
this.playerEventTarget_ = null;
/** @private {shaka.util.EventManager} */
this.eventManager_ = null;
/** @private {shaka.cast.CastSender} */
this.sender_ = new shaka.cast.CastSender(
receiverAppId,
this.onCastStatusChanged_.bind(this),
this.onRemoteEvent_.bind(this),
this.onResumeLocal_.bind(this),
this.getInitState_.bind(this));
this.init_();
};
goog.inherits(shaka.cast.CastProxy, shaka.util.FakeEventTarget);
/**
* Destroys the proxy and the underlying local Player.
*
* @override
* @export
*/
shaka.cast.CastProxy.prototype.destroy = function() {
var async = [
this.eventManager_ ? this.eventManager_.destroy() : null,
this.localPlayer_ ? this.localPlayer_.destroy() : null,
this.sender_ ? this.sender_.destroy() : null
];
this.localVideo_ = null;
this.localPlayer_ = null;
this.videoProxy_ = null;
this.playerProxy_ = null;
this.eventManager_ = null;
this.sender_ = null;
return Promise.all(async);
};
/**
* @event shaka.cast.CastProxy.CastStatusChangedEvent
* @description Fired when cast status changes. The status change will be
* reflected in canCast() and isCasting().
* @property {string} type
* 'caststatuschanged'
* @exportDoc
*/
/**
* Get a proxy for the video element that delegates to local and remote video
* elements as appropriate.
*
* @suppress {invalidCasts} to cast proxy Objects to unrelated types
* @return {HTMLMediaElement}
* @export
*/
shaka.cast.CastProxy.prototype.getVideo = function() {
return /** @type {HTMLMediaElement} */(this.videoProxy_);
};
/**
* Get a proxy for the Player that delegates to local and remote Player objects
* as appropriate.
*
* @suppress {invalidCasts} to cast proxy Objects to unrelated types
* @return {shaka.Player}
* @export
*/
shaka.cast.CastProxy.prototype.getPlayer = function() {
return /** @type {shaka.Player} */(this.playerProxy_);
};
/**
* @return {boolean} True if the cast API is available and there are receivers.
* @export
*/
shaka.cast.CastProxy.prototype.canCast = function() {
return this.sender_ ?
this.sender_.apiReady() && this.sender_.hasReceivers() :
false;
};
/**
* @return {boolean} True if we are currently casting.
* @export
*/
shaka.cast.CastProxy.prototype.isCasting = function() {
return this.sender_ ? this.sender_.isCasting() : false;
};
/**
* @return {string} The name of the Cast receiver device, if isCasting().
* @export
*/
shaka.cast.CastProxy.prototype.receiverName = function() {
return this.sender_ ? this.sender_.receiverName() : '';
};
/**
* @return {!Promise} Resolved when connected to a receiver. Rejected if the
* connection fails or is canceled by the user.
* @export
*/
shaka.cast.CastProxy.prototype.cast = function() {
var initState = this.getInitState_();
// TODO: transfer manually-selected tracks?
// TODO: transfer side-loaded text tracks?
return this.sender_.cast(initState).then(function() {
// Unload the local manifest when casting succeeds.
return this.localPlayer_.unload();
}.bind(this));
};
/**
* Set application-specific data.
*
* @param {Object} appData Application-specific data to relay to the receiver.
* @export
*/
shaka.cast.CastProxy.prototype.setAppData = function(appData) {
this.sender_.setAppData(appData);
};
/**
* Show a dialog where user can choose to disconnect from the cast connection.
* @export
*/
shaka.cast.CastProxy.prototype.suggestDisconnect = function() {
this.sender_.showDisconnectDialog();
};
/**
* Initialize the Proxies and the Cast sender.
* @private
*/
shaka.cast.CastProxy.prototype.init_ = function() {
this.sender_.init();
this.eventManager_ = new shaka.util.EventManager();
shaka.cast.CastUtils.VideoEvents.forEach(function(name) {
this.eventManager_.listen(this.localVideo_, name,
this.videoProxyLocalEvent_.bind(this));
}.bind(this));
shaka.cast.CastUtils.PlayerEvents.forEach(function(name) {
this.eventManager_.listen(this.localPlayer_, name,
this.playerProxyLocalEvent_.bind(this));
}.bind(this));
// We would like to use Proxy here, but it is not supported on IE11 or Safari.
this.videoProxy_ = {};
for (var k in this.localVideo_) {
Object.defineProperty(this.videoProxy_, k, {
configurable: false,
enumerable: true,
get: this.videoProxyGet_.bind(this, k),
set: this.videoProxySet_.bind(this, k)
});
}
this.playerProxy_ = {};
for (var k in /** @type {Object} */(this.localPlayer_)) {
Object.defineProperty(this.playerProxy_, k, {
configurable: false,
enumerable: true,
get: this.playerProxyGet_.bind(this, k)
});
}
this.videoEventTarget_ = new shaka.util.FakeEventTarget();
this.videoEventTarget_.dispatchTarget =
/** @type {EventTarget} */(this.videoProxy_);
this.playerEventTarget_ = new shaka.util.FakeEventTarget();
this.playerEventTarget_.dispatchTarget =
/** @type {EventTarget} */(this.playerProxy_);
};
/**
* @return {shaka.cast.CastUtils.InitStateType} initState Video and player state
* to be sent to the receiver.
* @private
*/
shaka.cast.CastProxy.prototype.getInitState_ = function() {
var initState = {
'video': {},
'player': {},
'playerAfterLoad': {},
'manifest': this.localPlayer_.getManifestUri(),
'startTime': null
};
// Pause local playback before capturing state.
this.localVideo_.pause();
shaka.cast.CastUtils.VideoInitStateAttributes.forEach(function(name) {
initState['video'][name] = this.localVideo_[name];
}.bind(this));
// If the video is still playing, set the startTime.
// Has no effect if nothing is loaded.
if (!this.localVideo_.ended) {
initState['startTime'] = this.localVideo_.currentTime;
}
shaka.cast.CastUtils.PlayerInitState.forEach(function(pair) {
var getter = pair[0];
var setter = pair[1];
var value = /** @type {Object} */(this.localPlayer_)[getter]();
initState['player'][setter] = value;
}.bind(this));
shaka.cast.CastUtils.PlayerInitAfterLoadState.forEach(function(pair) {
var getter = pair[0];
var setter = pair[1];
var value = /** @type {Object} */(this.localPlayer_)[getter]();
initState['playerAfterLoad'][setter] = value;
}.bind(this));
return initState;
};
/**
* Dispatch an event to notify the app that the status has changed.
* @private
*/
shaka.cast.CastProxy.prototype.onCastStatusChanged_ = function() {
var event = new shaka.util.FakeEvent('caststatuschanged');
this.dispatchEvent(event);
};
/**
* Transfer remote state back and resume local playback.
* @private
*/
shaka.cast.CastProxy.prototype.onResumeLocal_ = function() {
// Transfer back the player state.
shaka.cast.CastUtils.PlayerInitState.forEach(function(pair) {
var getter = pair[0];
var setter = pair[1];
var value = this.sender_.get('player', getter)();
/** @type {Object} */(this.localPlayer_)[setter](value);
}.bind(this));
// Get the most recent manifest URI and ended state.
var manifestUri = this.sender_.get('player', 'getManifestUri')();
var ended = this.sender_.get('video', 'ended');
var manifestReady = Promise.resolve();
var autoplay = this.localVideo_.autoplay;
var startTime = null;
// If the video is still playing, set the startTime.
// Has no effect if nothing is loaded.
if (!ended) {
startTime = this.sender_.get('video', 'currentTime');
}
// Now load the manifest, if present.
if (manifestUri) {
// Don't autoplay the content until we finish setting up initial state.
this.localVideo_.autoplay = false;
manifestReady = this.localPlayer_.load(manifestUri, startTime);
// Pass any errors through to the app.
manifestReady.catch(function(error) {
goog.asserts.assert(error instanceof shaka.util.Error,
'Wrong error type!');
var event = new shaka.util.FakeEvent('error', { 'detail': error });
this.localPlayer_.dispatchEvent(event);
}.bind(this));
}
// Get the video state into a temp variable since we will apply it async.
var videoState = {};
shaka.cast.CastUtils.VideoInitStateAttributes.forEach(function(name) {
videoState[name] = this.sender_.get('video', name);
}.bind(this));
// Finally, take on video state and player's "after load" state.
manifestReady.then(function() {
shaka.cast.CastUtils.VideoInitStateAttributes.forEach(function(name) {
this.localVideo_[name] = videoState[name];
}.bind(this));
shaka.cast.CastUtils.PlayerInitAfterLoadState.forEach(function(pair) {
var getter = pair[0];
var setter = pair[1];
var value = this.sender_.get('player', getter)();
/** @type {Object} */(this.localPlayer_)[setter](value);
}.bind(this));
// Restore original autoplay setting.
this.localVideo_.autoplay = autoplay;
if (manifestUri) {
// Resume playback with transferred state.
this.localVideo_.play();
}
}.bind(this));
};
/**
* @param {string} name
* @return {?}
* @private
*/
shaka.cast.CastProxy.prototype.videoProxyGet_ = function(name) {
if (name == 'addEventListener') {
return this.videoEventTarget_.addEventListener.bind(
this.videoEventTarget_);
}
if (name == 'removeEventListener') {
return this.videoEventTarget_.removeEventListener.bind(
this.videoEventTarget_);
}
// If we are casting, but the first update has not come in yet, use local
// values, but not local methods.
if (this.sender_.isCasting() && !this.sender_.hasRemoteProperties()) {
var value = this.localVideo_[name];
if (typeof value != 'function') {
return value;
}
}
// Use local values and methods if we are not casting.
if (!this.sender_.isCasting()) {
var value = this.localVideo_[name];
if (typeof value == 'function') {
value = value.bind(this.localVideo_);
}
return value;
}
return this.sender_.get('video', name);
};
/**
* @param {string} name
* @param {?} value
* @private
*/
shaka.cast.CastProxy.prototype.videoProxySet_ = function(name, value) {
if (!this.sender_.isCasting()) {
this.localVideo_[name] = value;
return;
}
this.sender_.set('video', name, value);
};
/**
* @param {!Event} event
* @private
*/
shaka.cast.CastProxy.prototype.videoProxyLocalEvent_ = function(event) {
if (this.sender_.isCasting()) {
// Ignore any unexpected local events while casting. Events can still be
// fired by the local video and Player when we unload() after the Cast
// connection is complete.
return;
}
// Convert this real Event into a FakeEvent for dispatch from our
// FakeEventListener.
var fakeEvent = new shaka.util.FakeEvent(event.type, event);
this.videoEventTarget_.dispatchEvent(fakeEvent);
};
/**
* @param {string} name
* @return {?}
* @private
*/
shaka.cast.CastProxy.prototype.playerProxyGet_ = function(name) {
if (name == 'addEventListener') {
return this.playerEventTarget_.addEventListener.bind(
this.playerEventTarget_);
}
if (name == 'removeEventListener') {
return this.playerEventTarget_.removeEventListener.bind(
this.playerEventTarget_);
}
if (name == 'getNetworkingEngine') {
// Always returns a local instance, in case you need to make a request.
// Issues a warning, in case you think you are making a remote request
// or affecting remote filters.
if (this.sender_.isCasting()) {
shaka.log.warning('NOTE: getNetworkingEngine() is always local!');
}
return this.localPlayer_.getNetworkingEngine.bind(this.localPlayer_);
}
// If we are casting, but the first update has not come in yet, use local
// getters, but not local methods.
if (this.sender_.isCasting() && !this.sender_.hasRemoteProperties()) {
if (shaka.cast.CastUtils.PlayerGetterMethods.indexOf(name) >= 0) {
var value = /** @type {Object} */(this.localPlayer_)[name];
goog.asserts.assert(typeof value == 'function', 'only methods on Player');
return value.bind(this.localPlayer_);
}
}
// Use local getters and methods if we are not casting.
if (!this.sender_.isCasting()) {
var value = /** @type {Object} */(this.localPlayer_)[name];
goog.asserts.assert(typeof value == 'function', 'only methods on Player');
return value.bind(this.localPlayer_);
}
return this.sender_.get('player', name);
};
/**
* @param {!Event} event
* @private
*/
shaka.cast.CastProxy.prototype.playerProxyLocalEvent_ = function(event) {
if (this.sender_.isCasting()) {
// Ignore any unexpected local events while casting.
return;
}
this.playerEventTarget_.dispatchEvent(event);
};
/**
* @param {string} targetName
* @param {!shaka.util.FakeEvent} event
* @private
*/
shaka.cast.CastProxy.prototype.onRemoteEvent_ = function(targetName, event) {
goog.asserts.assert(this.sender_.isCasting(),
'Should only receive remote events while casting');
if (!this.sender_.isCasting()) {
// Ignore any unexpected remote events.
return;
}
if (targetName == 'video') {
this.videoEventTarget_.dispatchEvent(event);
} else if (targetName == 'player') {
this.playerEventTarget_.dispatchEvent(event);
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.cast.CastReceiver');
goog.require('goog.asserts');
goog.require('shaka.cast.CastUtils');
goog.require('shaka.log');
goog.require('shaka.util.Error');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.IDestroyable');
/**
* A receiver to communicate between the Chromecast-hosted player and the
* sender application.
*
* @constructor
* @struct
* @param {!HTMLMediaElement} video The local video element associated with the
* local Player instance.
* @param {!shaka.Player} player A local Player instance.
* @param {function(Object)=} opt_appDataCallback A callback to handle
* application-specific data passed from the sender.
* @implements {shaka.util.IDestroyable}
* @extends {shaka.util.FakeEventTarget}
* @export
*/
shaka.cast.CastReceiver = function(video, player, opt_appDataCallback) {
shaka.util.FakeEventTarget.call(this);
/** @private {HTMLMediaElement} */
this.video_ = video;
/** @private {shaka.Player} */
this.player_ = player;
/** @private {Object} */
this.targets_ = {
'video': video,
'player': player
};
/** @private {?function(Object)} */
this.appDataCallback_ = opt_appDataCallback || function() {};
/** @private {boolean} */
this.isConnected_ = false;
/** @private {boolean} */
this.isIdle_ = true;
/** @private {cast.receiver.CastMessageBus} */
this.bus_ = null;
/** @private {?number} */
this.pollTimerId_ = null;
this.init_();
};
goog.inherits(shaka.cast.CastReceiver, shaka.util.FakeEventTarget);
/**
* @return {boolean} True if the cast API is available and there are receivers.
* @export
*/
shaka.cast.CastReceiver.prototype.isConnected = function() {
return this.isConnected_;
};
/**
* @return {boolean} True if the receiver is not currently doing loading or
* playing anything.
* @export
*/
shaka.cast.CastReceiver.prototype.isIdle = function() {
return this.isIdle_;
};
/**
* Destroys the underlying Player, then terminates the cast receiver app.
*
* @override
* @export
*/
shaka.cast.CastReceiver.prototype.destroy = function() {
var p = this.player_ ? this.player_.destroy() : Promise.resolve();
if (this.pollTimerId_ != null) {
window.clearTimeout(this.pollTimerId_);
}
this.video_ = null;
this.player_ = null;
this.targets_ = null;
this.appDataCallback_ = null;
this.isConnected_ = false;
this.isIdle_ = true;
this.bus_ = null;
this.pollTimerId_ = null;
return p.then(function() {
var manager = cast.receiver.CastReceiverManager.getInstance();
manager.stop();
});
};
/** @private */
shaka.cast.CastReceiver.prototype.init_ = function() {
var manager = cast.receiver.CastReceiverManager.getInstance();
manager.onSenderConnected = this.onSendersChanged_.bind(this);
manager.onSenderDisconnected = this.onSendersChanged_.bind(this);
manager.onSystemVolumeChanged = this.fakeVolumeChangeEvent_.bind(this);
this.bus_ = manager.getCastMessageBus(shaka.cast.CastUtils.MESSAGE_NAMESPACE);
this.bus_.onMessage = this.onMessage_.bind(this);
if (!COMPILED) {
// Sometimes it is useful to load the receiver app in Chrome to work on the
// UI. To avoid log spam caused by the SDK trying to connect to web sockets
// that don't exist, in uncompiled mode we check if the hosting browser is a
// Chromecast before starting the receiver manager. We wouldn't do browser
// detection except for debugging, so only do this in uncompiled mode.
var isChromecast = navigator.userAgent.indexOf('CrKey') >= 0;
if (isChromecast) {
manager.start();
}
} else {
manager.start();
}
shaka.cast.CastUtils.VideoEvents.forEach(function(name) {
this.video_.addEventListener(name, this.proxyEvent_.bind(this, 'video'));
}.bind(this));
shaka.cast.CastUtils.PlayerEvents.forEach(function(name) {
this.player_.addEventListener(name, this.proxyEvent_.bind(this, 'player'));
}.bind(this));
// In our tests, the original Chromecast seems to have trouble decoding above
// 1080p. It would be a waste to select a higher res anyway, given that the
// device only outputs 1080p to begin with.
// Chromecast has an extension to query the device/display's resolution.
if (cast.__platform__ && cast.__platform__.canDisplayType(
'video/mp4; codecs="avc1.640028"; width=3840; height=2160')) {
// The device & display can both do 4k. Assume a 4k limit.
this.player_.setMaxHardwareResolution(3840, 2160);
} else {
// Chromecast has always been able to do 1080p. Assume a 1080p limit.
this.player_.setMaxHardwareResolution(1920, 1080);
}
// Maintain idle state.
this.player_.addEventListener('loading', function() {
// No longer idle once loading. This allows us to show the spinner during
// the initial buffering phase.
this.isIdle_ = false;
this.onCastStatusChanged_();
}.bind(this));
this.video_.addEventListener('playing', function() {
// No longer idle once playing. This allows us to replay a video without
// reloading.
this.isIdle_ = false;
this.onCastStatusChanged_();
}.bind(this));
this.player_.addEventListener('unloading', function() {
// Go idle when unloading content.
this.isIdle_ = true;
this.onCastStatusChanged_();
}.bind(this));
this.video_.addEventListener('ended', function() {
// Go idle 5 seconds after 'ended', assuming we haven't started again or
// been destroyed.
window.setTimeout(function() {
if (this.video_ && this.video_.ended) {
this.isIdle_ = true;
this.onCastStatusChanged_();
}
}.bind(this), 5000);
}.bind(this));
// Do not start polling until after the sender's 'init' message is handled.
};
/** @private */
shaka.cast.CastReceiver.prototype.onSendersChanged_ = function() {
var manager = cast.receiver.CastReceiverManager.getInstance();
this.isConnected_ = manager.getSenders().length != 0;
this.onCastStatusChanged_();
};
/**
* Dispatch an event to notify the receiver app that the status has changed.
* @private
*/
shaka.cast.CastReceiver.prototype.onCastStatusChanged_ = function() {
// Do this asynchronously so that synchronous changes to idle state (such as
// Player calling unload() as part of load()) are coalesced before the event
// goes out.
Promise.resolve().then(function() {
var event = new shaka.util.FakeEvent('caststatuschanged');
this.dispatchEvent(event);
}.bind(this));
};
/**
* Take on initial state from the sender.
* @param {shaka.cast.CastUtils.InitStateType} initState
* @param {Object} appData
* @private
*/
shaka.cast.CastReceiver.prototype.initState_ = function(initState, appData) {
// Take on player state first.
for (var k in initState['player']) {
var v = initState['player'][k];
// All player state vars are setters to be called.
/** @type {Object} */(this.player_)[k](v);
}
// Now process custom app data, which may add additional player configs:
this.appDataCallback_(appData);
var manifestReady = Promise.resolve();
var autoplay = this.video_.autoplay;
// Now load the manifest, if present.
if (initState['manifest']) {
// Don't autoplay the content until we finish setting up initial state.
this.video_.autoplay = false;
manifestReady = this.player_.load(
initState['manifest'], initState['startTime']);
// Pass any errors through to the app.
manifestReady.catch(function(error) {
goog.asserts.assert(error instanceof shaka.util.Error,
'Wrong error type!');
var event = new shaka.util.FakeEvent('error', { 'detail': error });
this.player_.dispatchEvent(event);
}.bind(this));
}
// Finally, take on video state and player's "after load" state.
manifestReady.then(function() {
for (var k in initState['video']) {
var v = initState['video'][k];
this.video_[k] = v;
}
for (var k in initState['playerAfterLoad']) {
var v = initState['playerAfterLoad'][k];
// All player state vars are setters to be called.
/** @type {Object} */(this.player_)[k](v);
}
// Restore original autoplay setting.
this.video_.autoplay = autoplay;
if (initState['manifest']) {
// Resume playback with transferred state.
this.video_.play();
}
}.bind(this));
};
/**
* @param {string} targetName
* @param {!Event} event
* @private
*/
shaka.cast.CastReceiver.prototype.proxyEvent_ = function(targetName, event) {
// Poll and send an update right before we send the event. Some events
// indicate an attribute change, so that change should be visible when the
// event is handled.
this.pollAttributes_();
this.sendMessage_({
'type': 'event',
'targetName': targetName,
'event': event
});
};
/** @private */
shaka.cast.CastReceiver.prototype.pollAttributes_ = function() {
// The poll timer may have been pre-empted by an event.
// To avoid polling too often, we clear it here.
if (this.pollTimerId_ != null) {
window.clearTimeout(this.pollTimerId_);
}
// Since we know the timer has been cleared, start a new one now.
// This will be preempted by events, including 'timeupdate'.
this.pollTimerId_ = window.setTimeout(this.pollAttributes_.bind(this), 500);
var update = {
'video': {},
'player': {}
};
shaka.cast.CastUtils.VideoAttributes.forEach(function(name) {
update['video'][name] = this.video_[name];
}.bind(this));
shaka.cast.CastUtils.PlayerGetterMethods.forEach(function(name) {
update['player'][name] = /** @type {Object} */(this.player_)[name]();
}.bind(this));
// Volume attributes are tied to the system volume.
var manager = cast.receiver.CastReceiverManager.getInstance();
var systemVolume = manager.getSystemVolume();
if (systemVolume) {
update['video']['volume'] = systemVolume.level;
update['video']['muted'] = systemVolume.muted;
}
this.sendMessage_({
'type': 'update',
'update': update
});
};
/**
* Dispatch a fake 'volumechange' event to mimic the video element, since volume
* changes are routed to the system volume on the receiver.
* @private
*/
shaka.cast.CastReceiver.prototype.fakeVolumeChangeEvent_ = function() {
// Volume attributes are tied to the system volume.
var manager = cast.receiver.CastReceiverManager.getInstance();
var systemVolume = manager.getSystemVolume();
goog.asserts.assert(systemVolume, 'System volume should not be null!');
if (systemVolume) {
// Send an update message with just the latest volume level and muted state.
this.sendMessage_({
'type': 'update',
'update': {
'video': {
'volume': systemVolume.level,
'muted': systemVolume.muted
}
}
});
}
// Send another message with a 'volumechange' event to update the sender's UI.
this.sendMessage_({
'type': 'event',
'targetName': 'video',
'event': {'type': 'volumechange'}
});
};
/**
* Since this method is in the compiled library, make sure all messages are
* read with quoted properties.
* @param {cast.receiver.CastMessageBus.Event} event
* @private
*/
shaka.cast.CastReceiver.prototype.onMessage_ = function(event) {
var message = shaka.cast.CastUtils.deserialize(event.data);
shaka.log.debug('CastReceiver: message', message);
switch (message['type']) {
case 'init':
this.initState_(message['initState'], message['appData']);
// The sender is supposed to reflect the cast system volume after
// connecting. Using fakeVolumeChangeEvent_() would create a race on the
// sender side, since it would have volume properties, but no others.
// This would lead to hasRemoteProperties() being true, even though a
// complete set had never been sent.
// Now that we have init state, this is a good time for the first update
// message anyway.
this.pollAttributes_();
break;
case 'appData':
this.appDataCallback_(message['appData']);
break;
case 'set':
var targetName = message['targetName'];
var property = message['property'];
var value = message['value'];
if (targetName == 'video') {
// Volume attributes must be rerouted to the system.
var manager = cast.receiver.CastReceiverManager.getInstance();
if (property == 'volume') {
manager.setSystemVolumeLevel(value);
break;
} else if (property == 'muted') {
manager.setSystemVolumeMuted(value);
break;
}
}
this.targets_[targetName][property] = value;
break;
case 'call':
var targetName = message['targetName'];
var methodName = message['methodName'];
var args = message['args'];
var target = this.targets_[targetName];
target[methodName].apply(target, args);
break;
case 'asyncCall':
var targetName = message['targetName'];
var methodName = message['methodName'];
var args = message['args'];
var id = message['id'];
var senderId = event.senderId;
var target = this.targets_[targetName];
var p = target[methodName].apply(target, args);
// Replies must go back to the specific sender who initiated, so that we
// don't have to deal with conflicting IDs between senders.
p.then(this.sendAsyncComplete_.bind(this, senderId, id, /* error */ null),
this.sendAsyncComplete_.bind(this, senderId, id));
break;
}
};
/**
* Tell the sender that the async operation is complete.
* @param {string} senderId
* @param {string} id
* @param {shaka.util.Error} error
* @private
*/
shaka.cast.CastReceiver.prototype.sendAsyncComplete_ =
function(senderId, id, error) {
this.sendMessage_({
'type': 'asyncComplete',
'id': id,
'error': error
}, senderId);
};
/**
* Since this method is in the compiled library, make sure all messages passed
* in here were created with quoted property names.
* @param {!Object} message
* @param {string=} opt_senderId
* @private
*/
shaka.cast.CastReceiver.prototype.sendMessage_ =
function(message, opt_senderId) {
// Cuts log spam when debugging the receiver UI in Chrome.
if (!this.isConnected_) return;
var serialized = shaka.cast.CastUtils.serialize(message);
if (opt_senderId) {
this.bus_.getCastChannel(opt_senderId).send(serialized);
} else {
this.bus_.broadcast(serialized);
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.cast.CastSender');
goog.require('goog.asserts');
goog.require('shaka.cast.CastUtils');
goog.require('shaka.log');
goog.require('shaka.util.Error');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.PublicPromise');
/**
* @constructor
* @struct
* @param {string} receiverAppId The ID of the cast receiver application.
* @param {function()} onStatusChanged A callback invoked when the cast status
* changes.
* @param {function(string, !shaka.util.FakeEvent)} onRemoteEvent A callback
* invoked with target name and event when a remote event is received.
* @param {function()} onResumeLocal A callback invoked when the local player
* should resume playback. Called before cached remote state is wiped.
* @param {function()} onInitStateRequired A callback to get local player's.
* state. Invoked when casting is initiated from Chrome's cast button.
* @implements {shaka.util.IDestroyable}
*/
shaka.cast.CastSender =
function(receiverAppId, onStatusChanged, onRemoteEvent, onResumeLocal,
onInitStateRequired) {
/** @private {string} */
this.receiverAppId_ = receiverAppId;
/** @private {?function()} */
this.onStatusChanged_ = onStatusChanged;
/** @private {?function(string, !shaka.util.FakeEvent)} */
this.onRemoteEvent_ = onRemoteEvent;
/** @private {?function()} */
this.onResumeLocal_ = onResumeLocal;
/** @private {?function()} */
this.onInitStateRequired_ = onInitStateRequired;
/** @private {boolean} */
this.apiReady_ = false;
/** @private {boolean} */
this.hasReceivers_ = false;
/** @private {boolean} */
this.isCasting_ = false;
/** @private {string} */
this.receiverName_ = '';
/** @private {Object} */
this.appData_ = null;
/** @private {chrome.cast.Session} */
this.session_ = null;
/** @private {Object} */
this.cachedProperties_ = {
'video': {},
'player': {}
};
/** @private {number} */
this.nextAsyncCallId_ = 0;
/** @private {Object.<string, !shaka.util.PublicPromise>} */
this.asyncCallPromises_ = {};
/** @private {shaka.util.PublicPromise} */
this.castPromise_ = null;
};
/** @override */
shaka.cast.CastSender.prototype.destroy = function() {
this.rejectAllPromises_();
if (this.session_) {
this.session_.stop(function() {}, function() {});
this.session_ = null;
}
this.onStatusChanged_ = null;
this.onRemoteEvent_ = null;
this.onResumeLocal_ = null;
this.apiReady_ = false;
this.hasReceivers_ = false;
this.isCasting_ = false;
this.appData_ = null;
this.session_ = null;
this.cachedProperties_ = null;
this.asyncCallPromises_ = null;
this.castPromise_ = null;
return Promise.resolve();
};
/**
* @return {boolean} True if the cast API is available.
*/
shaka.cast.CastSender.prototype.apiReady = function() {
return this.apiReady_;
};
/**
* @return {boolean} True if there are receivers.
*/
shaka.cast.CastSender.prototype.hasReceivers = function() {
return this.hasReceivers_;
};
/**
* @return {boolean} True if we are currently casting.
*/
shaka.cast.CastSender.prototype.isCasting = function() {
return this.isCasting_;
};
/**
* @return {string} The name of the Cast receiver device, if isCasting().
*/
shaka.cast.CastSender.prototype.receiverName = function() {
return this.receiverName_;
};
/**
* @return {boolean} True if we have a cache of remote properties from the
* receiver.
*/
shaka.cast.CastSender.prototype.hasRemoteProperties = function() {
return Object.keys(this.cachedProperties_['video']).length != 0;
};
/**
* Initialize the Cast API.
*/
shaka.cast.CastSender.prototype.init = function() {
// Check for the cast extension.
if (!window.chrome || !chrome.cast || !chrome.cast.isAvailable) {
// Not available yet, so wait to be notified if/when it is available.
window.__onGCastApiAvailable = (function(loaded) {
if (loaded) {
this.init();
}
}).bind(this);
return;
}
// The API is now available.
delete window.__onGCastApiAvailable;
this.apiReady_ = true;
this.onStatusChanged_();
var sessionRequest = new chrome.cast.SessionRequest(this.receiverAppId_);
var apiConfig = new chrome.cast.ApiConfig(sessionRequest,
this.onExistingSessionJoined_.bind(this),
this.onReceiverStatusChanged_.bind(this),
'origin_scoped');
// TODO: have never seen this fail. when would it and how should we react?
chrome.cast.initialize(apiConfig,
function() { shaka.log.debug('CastSender: init'); },
function(error) { shaka.log.error('CastSender: init error', error); });
};
/**
* Set application-specific data.
*
* @param {Object} appData Application-specific data to relay to the receiver.
*/
shaka.cast.CastSender.prototype.setAppData = function(appData) {
this.appData_ = appData;
if (this.isCasting_) {
this.sendMessage_({
'type': 'appData',
'appData': this.appData_
});
}
};
/**
* @param {shaka.cast.CastUtils.InitStateType} initState Video and player state
* to be sent to the receiver.
* @return {!Promise} Resolved when connected to a receiver. Rejected if the
* connection fails or is canceled by the user.
*/
shaka.cast.CastSender.prototype.cast = function(initState) {
if (!this.apiReady_) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.CAST,
shaka.util.Error.Code.CAST_API_UNAVAILABLE));
}
if (!this.hasReceivers_) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.CAST,
shaka.util.Error.Code.NO_CAST_RECEIVERS));
}
if (this.isCasting_) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.CAST,
shaka.util.Error.Code.ALREADY_CASTING));
}
this.castPromise_ = new shaka.util.PublicPromise();
chrome.cast.requestSession(
this.onSessionInitiated_.bind(this, initState),
this.onConnectionError_.bind(this));
return this.castPromise_;
};
/**
* Shows user a cast dialog where they can choose to stop
* casting. Relies on Chrome to perform disconnect if they do.
* Doesn't do anything if not connected.
*/
shaka.cast.CastSender.prototype.showDisconnectDialog = function() {
if (!this.isCasting_) {
return;
}
var initState = this.onInitStateRequired_();
chrome.cast.requestSession(
this.onSessionInitiated_.bind(this, initState),
this.onConnectionError_.bind(this));
};
/**
* Getter for properties of remote objects.
* @param {string} targetName
* @param {string} property
* @return {?}
*/
shaka.cast.CastSender.prototype.get = function(targetName, property) {
goog.asserts.assert(targetName == 'video' || targetName == 'player',
'Unexpected target name');
if (targetName == 'video') {
if (shaka.cast.CastUtils.VideoVoidMethods.indexOf(property) >= 0) {
return this.remoteCall_.bind(this, targetName, property);
}
} else if (targetName == 'player') {
if (shaka.cast.CastUtils.PlayerVoidMethods.indexOf(property) >= 0) {
return this.remoteCall_.bind(this, targetName, property);
}
if (shaka.cast.CastUtils.PlayerPromiseMethods.indexOf(property) >= 0) {
return this.remoteAsyncCall_.bind(this, targetName, property);
}
if (shaka.cast.CastUtils.PlayerGetterMethods.indexOf(property) >= 0) {
return this.propertyGetter_.bind(this, targetName, property);
}
}
return this.propertyGetter_(targetName, property);
};
/**
* Setter for properties of remote objects.
* @param {string} targetName
* @param {string} property
* @param {?} value
*/
shaka.cast.CastSender.prototype.set = function(targetName, property, value) {
goog.asserts.assert(targetName == 'video' || targetName == 'player',
'Unexpected target name');
this.cachedProperties_[targetName][property] = value;
this.sendMessage_({
'type': 'set',
'targetName': targetName,
'property': property,
'value': value
});
};
/**
* @param {shaka.cast.CastUtils.InitStateType} initState
* @param {chrome.cast.Session} session
* @private
*/
shaka.cast.CastSender.prototype.onSessionInitiated_ =
function(initState, session) {
shaka.log.debug('CastSender: onSessionInitiated');
this.onSessionCreated_(session);
this.sendMessage_({
'type': 'init',
'initState': initState,
'appData': this.appData_
});
this.castPromise_.resolve();
};
/**
* @param {chrome.cast.Error} error
* @private
*/
shaka.cast.CastSender.prototype.onConnectionError_ = function(error) {
// Default error code:
var code = shaka.util.Error.Code.UNEXPECTED_CAST_ERROR;
switch (error.code) {
case 'cancel':
code = shaka.util.Error.Code.CAST_CANCELED_BY_USER;
break;
case 'timeout':
code = shaka.util.Error.Code.CAST_CONNECTION_TIMED_OUT;
break;
case 'receiver_unavailable':
code = shaka.util.Error.Code.CAST_RECEIVER_APP_UNAVAILABLE;
break;
}
this.castPromise_.reject(new shaka.util.Error(
shaka.util.Error.Category.CAST,
code,
error));
};
/**
* @param {string} targetName
* @param {string} property
* @return {?}
* @private
*/
shaka.cast.CastSender.prototype.propertyGetter_ =
function(targetName, property) {
goog.asserts.assert(targetName == 'video' || targetName == 'player',
'Unexpected target name');
return this.cachedProperties_[targetName][property];
};
/**
* @param {string} targetName
* @param {string} methodName
* @private
*/
shaka.cast.CastSender.prototype.remoteCall_ =
function(targetName, methodName) {
goog.asserts.assert(targetName == 'video' || targetName == 'player',
'Unexpected target name');
var args = Array.prototype.slice.call(arguments, 2);
this.sendMessage_({
'type': 'call',
'targetName': targetName,
'methodName': methodName,
'args': args
});
};
/**
* @param {string} targetName
* @param {string} methodName
* @return {!Promise}
* @private
*/
shaka.cast.CastSender.prototype.remoteAsyncCall_ =
function(targetName, methodName) {
goog.asserts.assert(targetName == 'video' || targetName == 'player',
'Unexpected target name');
var args = Array.prototype.slice.call(arguments, 2);
var p = new shaka.util.PublicPromise();
var id = this.nextAsyncCallId_.toString();
this.nextAsyncCallId_++;
this.asyncCallPromises_[id] = p;
this.sendMessage_({
'type': 'asyncCall',
'targetName': targetName,
'methodName': methodName,
'args': args,
'id': id
});
return p;
};
/**
* @param {chrome.cast.Session} session
* @private
*/
shaka.cast.CastSender.prototype.onExistingSessionJoined_ = function(session) {
shaka.log.debug('CastSender: onExistingSessionJoined');
var initState = this.onInitStateRequired_();
this.castPromise_ = new shaka.util.PublicPromise();
this.onSessionInitiated_(initState, session);
};
/**
* @param {string} availability
* @private
*/
shaka.cast.CastSender.prototype.onReceiverStatusChanged_ =
function(availability) {
// The cast extension is telling us whether there are any cast receiver
// devices available.
shaka.log.debug('CastSender: receiver status', availability);
this.hasReceivers_ = availability == 'available';
this.onStatusChanged_();
};
/**
* @param {chrome.cast.Session} session
* @private
*/
shaka.cast.CastSender.prototype.onSessionCreated_ = function(session) {
this.session_ = session;
this.session_.addUpdateListener(this.onConnectionStatusChanged_.bind(this));
this.session_.addMessageListener(
shaka.cast.CastUtils.MESSAGE_NAMESPACE,
this.onMessageReceived_.bind(this));
this.onConnectionStatusChanged_();
};
/**
* @private
*/
shaka.cast.CastSender.prototype.onConnectionStatusChanged_ = function() {
var connected = this.session_ ? this.session_.status == 'connected' : false;
shaka.log.debug('CastSender: connection status', connected);
if (this.isCasting_ && !connected) {
// Tell CastProxy to transfer state back to local player.
this.onResumeLocal_();
// Clear whatever we have cached.
for (var targetName in this.cachedProperties_) {
this.cachedProperties_[targetName] = {};
}
this.rejectAllPromises_();
}
this.isCasting_ = connected;
this.receiverName_ = connected ? this.session_.receiver.friendlyName : '';
this.onStatusChanged_();
};
/**
* Reject any async call promises that are still pending.
* @private
*/
shaka.cast.CastSender.prototype.rejectAllPromises_ = function() {
for (var id in this.asyncCallPromises_) {
var p = this.asyncCallPromises_[id];
delete this.asyncCallPromises_[id];
// Reject pending async operations as if they were interrupted.
// At the moment, load() is the only async operation we are worried
// about.
p.reject(new shaka.util.Error(
shaka.util.Error.Category.PLAYER,
shaka.util.Error.Code.LOAD_INTERRUPTED));
}
};
/**
* Since this method is in the compiled library, make sure all messages are
* read with quoted properties.
* @param {string} namespace
* @param {string} serialized
* @private
*/
shaka.cast.CastSender.prototype.onMessageReceived_ =
function(namespace, serialized) {
var message = shaka.cast.CastUtils.deserialize(serialized);
shaka.log.v2('CastSender: message', message);
switch (message['type']) {
case 'event':
var targetName = message['targetName'];
var event = message['event'];
var fakeEvent = new shaka.util.FakeEvent(event['type'], event);
this.onRemoteEvent_(targetName, fakeEvent);
break;
case 'update':
var update = message['update'];
for (var targetName in update) {
var target = this.cachedProperties_[targetName] || {};
for (var property in update[targetName]) {
target[property] = update[targetName][property];
}
}
break;
case 'asyncComplete':
var id = message['id'];
var error = message['error'];
var p = this.asyncCallPromises_[id];
delete this.asyncCallPromises_[id];
goog.asserts.assert(p, 'Unexpected async id');
if (!p) break;
if (error) {
// This is a hacky way to reconstruct the serialized error.
var reconstructedError = new shaka.util.Error(
error.category, error.code);
for (var k in error) {
(/** @type {Object} */(reconstructedError))[k] = error[k];
}
p.reject(reconstructedError);
} else {
p.resolve();
}
break;
}
};
/**
* Since this method is in the compiled library, make sure all messages passed
* in here were created with quoted property names.
* @param {!Object} message
* @private
*/
shaka.cast.CastSender.prototype.sendMessage_ = function(message) {
var serialized = shaka.cast.CastUtils.serialize(message);
// TODO: have never seen this fail. when would it and how should we react?
this.session_.sendMessage(shaka.cast.CastUtils.MESSAGE_NAMESPACE, serialized,
function() {}, // success callback
shaka.log.error); // error callback
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.cast.CastUtils');
goog.require('shaka.util.FakeEvent');
/**
* @namespace shaka.cast.CastUtils
* @summary A set of cast utility functions and variables shared between sender
* and receiver.
*/
/**
* HTMLMediaElement events that are proxied while casting.
* @const {!Array.<string>}
*/
shaka.cast.CastUtils.VideoEvents = [
'ended',
'play',
'playing',
'pause',
'pausing',
'ratechange',
'seeked',
'seeking',
'timeupdate',
'volumechange'
];
/**
* HTMLMediaElement attributes that are proxied while casting.
* @const {!Array.<string>}
*/
shaka.cast.CastUtils.VideoAttributes = [
'buffered',
'currentTime',
'duration',
'ended',
'loop',
'muted',
'paused',
'playbackRate',
'seeking',
'videoHeight',
'videoWidth',
'volume'
];
/**
* HTMLMediaElement attributes that are transferred when casting begins.
* @const {!Array.<string>}
*/
shaka.cast.CastUtils.VideoInitStateAttributes = [
'loop',
'playbackRate'
];
/**
* HTMLMediaElement methods with no return value that are proxied while casting.
* @const {!Array.<string>}
*/
shaka.cast.CastUtils.VideoVoidMethods = [
'pause',
'play'
];
/**
* Player events that are proxied while casting.
* @const {!Array.<string>}
*/
shaka.cast.CastUtils.PlayerEvents = [
'adaptation',
'buffering',
'emsg',
'error',
'loading',
'unloading',
'texttrackvisibility',
'trackschanged'
];
/**
* Player getter methods that are proxied while casting.
* @const {!Array.<string>}
*/
shaka.cast.CastUtils.PlayerGetterMethods = [
'drmInfo',
'getConfiguration',
'getManifestUri',
'getPlaybackRate',
'getTracks',
'getStats',
'isBuffering',
'isInProgress',
'isLive',
'isTextTrackVisible',
'keySystem',
'seekRange'
];
/**
* Player getter and setter methods that are used to transfer state when casting
* begins.
* @const {!Array.<!Array.<string>>}
*/
shaka.cast.CastUtils.PlayerInitState = [
['getConfiguration', 'configure']
];
/**
* Player getter and setter methods that are used to transfer state after
* after load() is resolved.
* @const {!Array.<!Array.<string>>}
*/
shaka.cast.CastUtils.PlayerInitAfterLoadState = [
['isTextTrackVisible', 'setTextTrackVisibility']
];
/**
* Player methods with no return value that are proxied while casting.
* @const {!Array.<string>}
*/
shaka.cast.CastUtils.PlayerVoidMethods = [
'addTextTrack',
'cancelTrickPlay',
'configure',
'resetConfiguration',
'selectTrack',
'setTextTrackVisibility',
'trickPlay'
];
/**
* Player methods returning a Promise that are proxied while casting.
* @const {!Array.<string>}
*/
shaka.cast.CastUtils.PlayerPromiseMethods = [
// The opt_manifestFactory method is not supported.
'load',
'unload'
];
/**
* @typedef {{
* video: Object,
* player: Object,
* manifest: ?string,
* startTime: ?number
* }}
* @property {Object} video
* Dictionary of video properties to be set.
* @property {Object} player
* Dictionary of player setters to be called.
* @property {?string} manifest
* The currently-selected manifest, if present.
* @property {?number} startTime
* The playback start time, if currently playing.
*/
shaka.cast.CastUtils.InitStateType;
/**
* The namespace for Shaka messages on the cast bus.
* @const {string}
*/
shaka.cast.CastUtils.MESSAGE_NAMESPACE = 'urn:x-cast:com.google.shaka.v2';
/**
* Serialize as JSON, but specially encode things JSON will not otherwise
* represent.
* @param {?} thing
* @return {string}
*/
shaka.cast.CastUtils.serialize = function(thing) {
return JSON.stringify(thing, function(key, value) {
if (key == 'manager') {
// ABR manager can't be serialized.
return undefined;
}
if (typeof value == 'function') {
// Functions can't be (safely) serialized.
return undefined;
}
if (value instanceof Event || value instanceof shaka.util.FakeEvent) {
// Events don't serialize to JSON well because of the DOM objects
// and other complex objects they contain. So we strip these out.
// Note that using Object.keys or JSON.stringify directly on the event
// will not capture its properties. We must use a for loop.
var simpleEvent = {};
for (var eventKey in value) {
var eventValue = value[eventKey];
if (eventValue && typeof eventValue == 'object') {
// Strip out non-null object types because they are complex and we
// don't need them.
} else if (eventKey in Event) {
// Strip out keys that are found on Event itself because they are
// class-level constants we don't need, like Event.MOUSEMOVE == 16.
} else {
simpleEvent[eventKey] = eventValue;
}
}
return simpleEvent;
}
if (value instanceof TimeRanges) {
// TimeRanges must be unpacked into plain data for serialization.
return shaka.cast.CastUtils.unpackTimeRanges_(value);
}
if (typeof value == 'number') {
// NaN and infinity cannot be represented directly in JSON.
if (isNaN(value)) return 'NaN';
if (isFinite(value)) return value;
if (value < 0) return '-Infinity';
return 'Infinity';
}
return value;
});
};
/**
* Deserialize JSON using our special encodings.
* @param {string} str
* @return {?}
*/
shaka.cast.CastUtils.deserialize = function(str) {
return JSON.parse(str, function(key, value) {
if (value == 'NaN') {
return NaN;
} else if (value == '-Infinity') {
return -Infinity;
} else if (value == 'Infinity') {
return Infinity;
} else if (value && typeof value == 'object' &&
value['__type__'] == 'TimeRanges') {
// TimeRanges objects have been unpacked and sent as plain data.
// Simulate the original TimeRanges object.
return shaka.cast.CastUtils.simulateTimeRanges_(value);
}
return value;
});
};
/**
* @param {!TimeRanges} ranges
* @return {Object}
* @private
*/
shaka.cast.CastUtils.unpackTimeRanges_ = function(ranges) {
var obj = {
'__type__': 'TimeRanges', // a signal to deserialize
'length': ranges.length,
'start': [],
'end': []
};
for (var i = 0; i < ranges.length; ++i) {
obj['start'].push(ranges.start(i));
obj['end'].push(ranges.end(i));
}
return obj;
};
/**
* Creates a simulated TimeRanges object from data sent by the cast receiver.
* @param {?} obj
* @return {{
* length: number,
* start: function(number): number,
* end: function(number): number
* }}
* @private
*/
shaka.cast.CastUtils.simulateTimeRanges_ = function(obj) {
return {
length: obj.length,
// NOTE: a more complete simulation would throw when |i| was out of range,
// but for simplicity we will assume a well-behaved application that uses
// length instead of catch to stop iterating.
start: function(i) { return obj.start[i]; },
end: function(i) { return obj.end[i]; }
};
};
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| content_protection.js | 1.08% | (1 / 93) | 0% | (0 / 49) | 0% | (0 / 13) | 1.09% | (1 / 92) | |
| dash_parser.js | 0.22% | (1 / 465) | 0% | (0 / 221) | 0% | (0 / 51) | 0.22% | (1 / 462) | |
| mpd_utils.js | 0.67% | (1 / 150) | 0% | (0 / 78) | 0% | (0 / 16) | 0.68% | (1 / 148) | |
| segment_base.js | 1.08% | (1 / 93) | 0% | (0 / 31) | 0% | (0 / 10) | 1.09% | (1 / 92) | |
| segment_list.js | 1.02% | (1 / 98) | 0% | (0 / 47) | 0% | (0 / 10) | 1.03% | (1 / 97) | |
| segment_template.js | 0.71% | (1 / 141) | 0% | (0 / 60) | 0% | (0 / 14) | 0.71% | (1 / 141) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.dash.ContentProtection');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.util.Error');
goog.require('shaka.util.Functional');
goog.require('shaka.util.MapUtils');
goog.require('shaka.util.Uint8ArrayUtils');
goog.require('shaka.util.XmlUtils');
/**
* @namespace shaka.dash.ContentProtection
* @summary A set of functions for parsing and interpreting ContentProtection
* elements.
*/
/**
* @typedef {{
* defaultKeyId: ?string,
* defaultInit: Array.<shakaExtern.InitDataOverride>,
* drmInfos: !Array.<shakaExtern.DrmInfo>,
* firstRepresentation: boolean
* }}
*
* @description
* Contains information about the ContentProtection elements found at the
* AdaptationSet level.
*
* @property {?string} defaultKeyId
* The default key ID to use. This is used by parseKeyIds as a default. This
* can be null to indicate that there is no default.
* @property {Array.<shakaExtern.InitDataOverride>} defaultInit
* The default init data override. This can be null to indicate that there
* is no default.
* @property {!Array.<shakaExtern.DrmInfo>} drmInfos
* The DrmInfo objects.
* @property {boolean} firstRepresentation
* True when first parsed; changed to false after the first call to
* parseKeyIds. This is used to determine if a dummy key-system should be
* overwritten; namely that the first representation can replace the dummy
* from the AdaptationSet.
*/
shaka.dash.ContentProtection.Context;
/**
* @typedef {{
* node: !Element,
* schemeUri: string,
* keyId: ?string,
* init: Array.<shakaExtern.InitDataOverride>
* }}
*
* @description
* The parsed result of a single ContentProtection element.
*
* @property {!Element} node
* The ContentProtection XML element.
* @property {string} schemeUri
* The scheme URI.
* @property {?string} keyId
* The default key ID, if present.
* @property {Array.<shakaExtern.InitDataOverride>} init
* The init data, if present. If there is no init data, it will be null. If
* this is non-null, there is at least one element.
*/
shaka.dash.ContentProtection.Element;
/**
* A map of scheme URI to key system name.
*
* @const {!Object.<string, string>}
* @private
*/
shaka.dash.ContentProtection.defaultKeySystems_ = {
'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey',
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha',
'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready',
'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime'
};
/**
* @const {string}
* @private
*/
shaka.dash.ContentProtection.MP4Protection_ =
'urn:mpeg:dash:mp4protection:2011';
/**
* Parses info from the ContentProtection elements at the AdaptationSet level.
*
* @param {!Array.<!Element>} elems
* @param {shakaExtern.DashContentProtectionCallback} callback
* @return {shaka.dash.ContentProtection.Context}
*/
shaka.dash.ContentProtection.parseFromAdaptationSet = function(
elems, callback) {
var ContentProtection = shaka.dash.ContentProtection;
var Functional = shaka.util.Functional;
var MapUtils = shaka.util.MapUtils;
var parsed = ContentProtection.parseElements_(elems);
// Find the default key ID and init data. Create a new array of all the
// non-CENC elements.
/** @type {Array.<shakaExtern.InitDataOverride>} */
var defaultInit = null;
var parsedNonCenc = parsed.filter(function(elem) {
if (elem.schemeUri == ContentProtection.MP4Protection_) {
goog.asserts.assert(!elem.init || elem.init.length,
'Init data must be null or non-empty.');
defaultInit = elem.init || defaultInit;
return false;
} else {
return true;
}
});
// Get the default key ID; if there are multiple, they must all match.
var keyIds = parsed.map(function(elem) { return elem.keyId; })
.filter(Functional.isNotNull);
/** @type {?string} */
var defaultKeyId = null;
if (keyIds.length > 0) {
defaultKeyId = keyIds[0];
if (keyIds.some(Functional.isNotEqualFunc(defaultKeyId))) {
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_CONFLICTING_KEY_IDS);
}
}
/** @type {!Array.<shakaExtern.DrmInfo>} */
var drmInfos = [];
if (parsedNonCenc.length > 0) {
drmInfos = ContentProtection.convertElements_(
defaultInit, callback, parsedNonCenc);
// If there are no drmInfos after parsing, then add a dummy entry. This may
// be removed in parseKeyIds.
if (drmInfos.length == 0) {
drmInfos = [ContentProtection.createDrmInfo_('', defaultInit)];
}
} else if (parsed.length > 0) {
// If there are only CENC element(s), then assume all key-systems are
// supported.
var keySystems = ContentProtection.defaultKeySystems_;
drmInfos =
MapUtils.values(keySystems)
.map(function(keySystem) {
return ContentProtection.createDrmInfo_(keySystem, defaultInit);
});
}
return {
defaultKeyId: defaultKeyId,
defaultInit: defaultInit,
drmInfos: drmInfos,
firstRepresentation: true
};
};
/**
* Parses the given ContentProtection elements found at the Representation
* level. This may update the |context|.
*
* @param {!Array.<!Element>} elems
* @param {shakaExtern.DashContentProtectionCallback} callback
* @param {shaka.dash.ContentProtection.Context} context
* @return {?string} The parsed key ID
*/
shaka.dash.ContentProtection.parseFromRepresentation = function(
elems, callback, context) {
var ContentProtection = shaka.dash.ContentProtection;
var repContext = ContentProtection.parseFromAdaptationSet(elems, callback);
if (context.firstRepresentation) {
var asUnknown = context.drmInfos.length == 1 &&
!context.drmInfos[0].keySystem;
var asUnencrypted = context.drmInfos.length == 0;
var repUnencrypted = repContext.drmInfos.length == 0;
// There are two cases when we need to replace the |drmInfos| in the context
// with those in the Representation:
// * The AdaptationSet does not list any ContentProtection.
// * The AdaptationSet only lists unknown key-systems.
if (asUnencrypted || (asUnknown && !repUnencrypted)) {
context.drmInfos = repContext.drmInfos;
}
context.firstRepresentation = false;
} else if (repContext.drmInfos.length > 0) {
// If this is not the first Representation, then we need to remove entries
// from the context that do not appear in this Representation.
context.drmInfos = context.drmInfos.filter(function(asInfo) {
return repContext.drmInfos.some(function(repInfo) {
return repInfo.keySystem == asInfo.keySystem;
});
});
// If we have filtered out all key-systems, throw an error.
if (context.drmInfos.length == 0) {
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_NO_COMMON_KEY_SYSTEM);
}
}
return repContext.defaultKeyId || context.defaultKeyId;
};
/**
* Creates a DrmInfo object from the given info.
*
* @param {string} keySystem
* @param {Array.<shakaExtern.InitDataOverride>} initData
* @return {shakaExtern.DrmInfo}
* @private
*/
shaka.dash.ContentProtection.createDrmInfo_ = function(keySystem, initData) {
return {
keySystem: keySystem,
licenseServerUri: '',
distinctiveIdentifierRequired: false,
persistentStateRequired: false,
audioRobustness: '',
videoRobustness: '',
serverCertificate: null,
initData: initData || [],
keyIds: []
};
};
/**
* Creates DrmInfo objects from the given element.
*
* @param {Array.<shakaExtern.InitDataOverride>} defaultInit
* @param {shakaExtern.DashContentProtectionCallback} callback
* @param {!Array.<shaka.dash.ContentProtection.Element>} elements
* @return {!Array.<shakaExtern.DrmInfo>}
* @private
*/
shaka.dash.ContentProtection.convertElements_ = function(
defaultInit, callback, elements) {
var Functional = shaka.util.Functional;
return elements.map(
/**
* @param {shaka.dash.ContentProtection.Element} element
* @return {!Array.<shakaExtern.DrmInfo>}
*/
function(element) {
var ContentProtection = shaka.dash.ContentProtection;
var keySystem = ContentProtection.defaultKeySystems_[element.schemeUri];
if (keySystem) {
goog.asserts.assert(!element.init || element.init.length,
'Init data must be null or non-empty.');
var initData = element.init || defaultInit;
return [ContentProtection.createDrmInfo_(keySystem, initData)];
} else {
goog.asserts.assert(
callback, 'ContentProtection callback is required');
return callback(element.node) || [];
}
}).reduce(Functional.collapseArrays, []);
};
/**
* Parses the given ContentProtection elements. If there is an error, it
* removes those elements.
*
* @param {!Array.<!Element>} elems
* @return {!Array.<shaka.dash.ContentProtection.Element>}
* @private
*/
shaka.dash.ContentProtection.parseElements_ = function(elems) {
var Functional = shaka.util.Functional;
return elems.map(
/**
* @param {!Element} elem
* @return {?shaka.dash.ContentProtection.Element}
*/
function(elem) {
/** @type {?string} */
var schemeUri = elem.getAttribute('schemeIdUri');
/** @type {?string} */
var keyId = elem.getAttribute('cenc:default_KID');
/** @type {!Array.<string>} */
var psshs = shaka.util.XmlUtils.findChildren(elem, 'cenc:pssh')
.map(shaka.util.XmlUtils.getContents);
if (!schemeUri) {
shaka.log.error('Missing required schemeIdUri attribute on',
'ContentProtection element', elem);
return null;
}
schemeUri = schemeUri.toLowerCase();
if (keyId) {
keyId = keyId.replace(/-/g, '').toLowerCase();
if (keyId.indexOf(' ') >= 0) {
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_MULTIPLE_KEY_IDS_NOT_SUPPORTED);
}
}
/** @type {!Array.<shakaExtern.InitDataOverride>} */
var init = [];
try {
init = psshs.map(function(pssh) {
/** @type {shakaExtern.InitDataOverride} */
var ret = {
initDataType: 'cenc',
initData: shaka.util.Uint8ArrayUtils.fromBase64(pssh)
};
return ret;
});
} catch (e) {
// Invalid PSSH data, ignore.
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_PSSH_BAD_ENCODING);
}
/** @type {shaka.dash.ContentProtection.Element} */
var element = {
node: elem,
schemeUri: schemeUri,
keyId: keyId,
init: (init.length > 0 ? init : null)
};
return element;
}).filter(Functional.isNotNull);
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.dash.DashParser');
goog.require('goog.asserts');
goog.require('shaka.dash.ContentProtection');
goog.require('shaka.dash.MpdUtils');
goog.require('shaka.dash.SegmentBase');
goog.require('shaka.dash.SegmentList');
goog.require('shaka.dash.SegmentTemplate');
goog.require('shaka.log');
goog.require('shaka.media.ManifestParser');
goog.require('shaka.media.PresentationTimeline');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.media.TextEngine');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.DataViewReader');
goog.require('shaka.util.Error');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.Functional');
goog.require('shaka.util.LanguageUtils');
goog.require('shaka.util.MapUtils');
goog.require('shaka.util.Mp4Parser');
goog.require('shaka.util.MultiMap');
goog.require('shaka.util.StreamUtils');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.XmlUtils');
/**
* Creates a new DASH parser.
*
* @struct
* @constructor
* @implements {shakaExtern.ManifestParser}
* @export
*/
shaka.dash.DashParser = function() {
/** @private {shaka.net.NetworkingEngine} */
this.networkingEngine_ = null;
/** @private {?shakaExtern.ManifestConfiguration} */
this.config_ = null;
/** @private {?function(shakaExtern.Period)} */
this.filterPeriod_ = null;
/** @private {?function(!shaka.util.Error)} */
this.onError_ = null;
/** @private {?function(!shaka.util.FakeEvent)} */
this.onEvent_ = null;
/** @private {!Array.<string>} */
this.manifestUris_ = [];
/** @private {?shakaExtern.Manifest} */
this.manifest_ = null;
/** @private {!Array.<string>} */
this.periodIds_ = [];
/** @private {number} */
this.globalId_ = 1;
/**
* A map of IDs to SegmentIndex objects.
* ID: Period@id,AdaptationSet@id,@Representation@id
* e.g.: '1,5,23'
* @private {!Object.<string, !shaka.media.SegmentIndex>}
*/
this.segmentIndexMap_ = {};
/**
* The update period in seconds; or 0 for no updates.
* @private {number}
*/
this.updatePeriod_ = 0;
/** @private {?number} */
this.updateTimer_ = null;
/** @private {!shakaExtern.ResponseFilter} */
this.emsgResponseFilter_ = this.emsgResponseFilter_.bind(this);
};
/**
* Contains the minimum amount of time, in seconds, between manifest update
* requests.
*
* @private
* @const {number}
*/
shaka.dash.DashParser.MIN_UPDATE_PERIOD_ = 3;
/**
* The default MPD@suggestedPresentationDelay in seconds.
*
* @private
* @const {number}
*/
shaka.dash.DashParser.DEFAULT_SUGGESTED_PRESENTATION_DELAY_ = 10;
/**
* @typedef {
* !function(!Array.<string>, ?number, ?number):!Promise.<!ArrayBuffer>
* }
*/
shaka.dash.DashParser.RequestInitSegmentCallback;
/**
* @typedef {{
* segmentBase: Element,
* segmentList: Element,
* segmentTemplate: Element,
* baseUris: !Array.<string>,
* width: (number|undefined),
* height: (number|undefined),
* contentType: string,
* mimeType: string,
* codecs: string,
* frameRate: (number|undefined),
* id: string
* }}
*
* @description
* A collection of elements and properties which are inherited across levels
* of a DASH manifest.
*
* @property {Element} segmentBase
* The XML node for SegmentBase.
* @property {Element} segmentList
* The XML node for SegmentList.
* @property {Element} segmentTemplate
* The XML node for SegmentTemplate.
* @property {!Array.<string>} baseUris
* An array of absolute base URIs for the frame.
* @property {(number|undefined)} width
* The inherited width value.
* @property {(number|undefined)} height
* The inherited height value.
* @property {string} contentType
* The inherited media type.
* @property {string} mimeType
* The inherited MIME type value.
* @property {string} codecs
* The inherited codecs value.
* @property {(number|undefined)} frameRate
* The inherited framerate value.
* @property {string} id
* The ID of the element.
*/
shaka.dash.DashParser.InheritanceFrame;
/**
* @typedef {{
* dynamic: boolean,
* presentationTimeline: !shaka.media.PresentationTimeline,
* period: ?shaka.dash.DashParser.InheritanceFrame,
* periodInfo: ?shaka.dash.DashParser.PeriodInfo,
* adaptationSet: ?shaka.dash.DashParser.InheritanceFrame,
* representation: ?shaka.dash.DashParser.InheritanceFrame,
* bandwidth: (number|undefined),
* indexRangeWarningGiven: boolean
* }}
*
* @description
* Contains context data for the streams.
*
* @property {boolean} dynamic
* True if the MPD is dynamic (not all segments available at once)
* @property {!shaka.media.PresentationTimeline} presentationTimeline
* The PresentationTimeline.
* @property {?shaka.dash.DashParser.InheritanceFrame} period
* The inheritance from the Period element.
* @property {?shaka.dash.DashParser.PeriodInfo} periodInfo
* The Period info for the current Period.
* @property {?shaka.dash.DashParser.InheritanceFrame} adaptationSet
* The inheritance from the AdaptationSet element.
* @property {?shaka.dash.DashParser.InheritanceFrame} representation
* The inheritance from the Representation element.
* @property {(number|undefined)} bandwidth
* The bandwidth of the Representation.
* @property {boolean} indexRangeWarningGiven
* True if the warning about SegmentURL@indexRange has been printed.
*/
shaka.dash.DashParser.Context;
/**
* @typedef {{
* start: number,
* duration: ?number,
* node: !Element,
* containsInband: boolean
* }}
*
* @description
* Contains information about a Period element.
*
* @property {number} start
* The start time of the period.
* @property {?number} duration
* The duration of the period; or null if the duration is not given. This
* will be non-null for all periods except the last.
* @property {!Element} node
* The XML Node for the Period.
* @property {boolean} containsInband
* Indicates whether a period contains inband information.
*/
shaka.dash.DashParser.PeriodInfo;
/**
* @typedef {{
* id: string,
* contentType: ?string,
* language: string,
* main: boolean,
* streams: !Array.<shakaExtern.Stream>,
* drmInfos: !Array.<shakaExtern.DrmInfo>,
* switchableIds: !Array.<string>,
* containsInband: boolean,
* representationIds: !Array.<string>
* }}
*
* @description
* Contains information about an AdaptationSet element.
*
* @property {string} id
* The unique ID of the adaptation set.
* @property {?string} contentType
* The content type of the AdaptationSet.
* @property {string} language
* The language of the AdaptationSet.
* @property {boolean} main
* Whether the AdaptationSet has the 'main' type.
* @property {!Array.<shakaExtern.Stream>} streams
* The streams this AdaptationSet contains.
* @property {!Array.<shakaExtern.DrmInfo>} drmInfos
* The DRM info for the AdaptationSet.
* @property {!Array.<string>} switchableIds
* An array of the IDs of the AdaptationSets it can switch to.
* @property {boolean} containsInband
* Signals whether AdaptationSet has inband content indicator on it.
* @property {!Array.<string>} representationIds
* An array of the IDs of the Representations this AdaptationSet contains.
*/
shaka.dash.DashParser.AdaptationInfo;
/**
* @typedef {{
* createSegmentIndex: shakaExtern.CreateSegmentIndexFunction,
* findSegmentPosition: shakaExtern.FindSegmentPositionFunction,
* getSegmentReference: shakaExtern.GetSegmentReferenceFunction
* }}
*
* @description
* Contains functions used to create and find segment references.
*
* @property {shakaExtern.CreateSegmentIndexFunction} createSegmentIndex
* The createSegmentIndex function.
* @property {shakaExtern.FindSegmentPositionFunction} findSegmentPosition
* The findSegmentPosition function.
* @property {shakaExtern.GetSegmentReferenceFunction} getSegmentReference
* The getSegmentReference function.
*/
shaka.dash.DashParser.SegmentIndexFunctions;
/**
* @typedef {{
* createSegmentIndex: shakaExtern.CreateSegmentIndexFunction,
* findSegmentPosition: shakaExtern.FindSegmentPositionFunction,
* getSegmentReference: shakaExtern.GetSegmentReferenceFunction,
* initSegmentReference: shaka.media.InitSegmentReference,
* presentationTimeOffset: (number|undefined)
* }}
*
* @description
* Contains information about a Stream. This is passed from the createStream
* methods.
*
* @property {shakaExtern.CreateSegmentIndexFunction} createSegmentIndex
* The createSegmentIndex function for the stream.
* @property {shakaExtern.FindSegmentPositionFunction} findSegmentPosition
* The findSegmentPosition function for the stream.
* @property {shakaExtern.GetSegmentReferenceFunction} getSegmentReference
* The getSegmentReference function for the stream.
* @property {shaka.media.InitSegmentReference} initSegmentReference
* The init segment for the stream.
* @property {(number|undefined)} presentationTimeOffset
* The presentationTimeOffset for the stream.
*/
shaka.dash.DashParser.StreamInfo;
/**
* @override
* @exportInterface
*/
shaka.dash.DashParser.prototype.configure = function(config) {
this.config_ = config;
};
/**
* @override
* @exportInterface
*/
shaka.dash.DashParser.prototype.start =
function(uri, networkingEngine, filterPeriod, onError, onEvent) {
goog.asserts.assert(this.config_, 'Must call configure() before start()!');
this.manifestUris_ = [uri];
this.networkingEngine_ = networkingEngine;
this.filterPeriod_ = filterPeriod;
this.onError_ = onError;
this.onEvent_ = onEvent;
return this.requestManifest_().then(function() {
if (this.networkingEngine_)
this.setUpdateTimer_(0);
return this.manifest_;
}.bind(this));
};
/**
* @override
* @exportInterface
*/
shaka.dash.DashParser.prototype.stop = function() {
if (this.networkingEngine_)
this.networkingEngine_.unregisterResponseFilter(this.emsgResponseFilter_);
this.networkingEngine_ = null;
this.filterPeriod_ = null;
this.onError_ = null;
this.onEvent_ = null;
this.config_ = null;
this.manifestUris_ = [];
this.manifest_ = null;
this.periodIds_ = [];
this.segmentIndexMap_ = {};
if (this.updateTimer_ != null) {
window.clearTimeout(this.updateTimer_);
this.updateTimer_ = null;
}
return Promise.resolve();
};
/**
* Makes a network request for the manifest and parses the resulting data.
*
* @return {!Promise}
* @private
*/
shaka.dash.DashParser.prototype.requestManifest_ = function() {
var requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
var request = shaka.net.NetworkingEngine.makeRequest(
this.manifestUris_, this.config_.retryParameters);
return this.networkingEngine_.request(requestType, request)
.then(function(response) {
// Detect calls to stop().
if (!this.networkingEngine_)
return;
// This may throw; but it will result in a failed promise.
return this.parseManifest_(response.data, response.uri);
}.bind(this));
};
/**
* Parses the manifest XML. This also handles updates and will update the
* stored manifest.
*
* @param {!ArrayBuffer} data
* @param {string} finalManifestUri The final manifest URI, which may
* differ from this.manifestUri_ if there has been a redirect.
* @return {!Promise}
* @throws shaka.util.Error When there is a parsing error.
* @private
*/
shaka.dash.DashParser.prototype.parseManifest_ =
function(data, finalManifestUri) {
var Error = shaka.util.Error;
var Functional = shaka.util.Functional;
var XmlUtils = shaka.util.XmlUtils;
var string = shaka.util.StringUtils.fromUTF8(data);
var parser = new DOMParser();
var xml = null;
var mpd = null;
try {
xml = parser.parseFromString(string, 'text/xml');
} catch (exception) {}
if (xml) {
// parseFromString returns a Document object. A Document is a Node but not
// an Element, so it cannot be used in XmlUtils (technically it can but the
// types don't match). The |documentElement| member defines the top-level
// element in the document.
if (xml.documentElement.tagName == 'MPD')
mpd = xml.documentElement;
}
if (!mpd) {
throw new Error(Error.Category.MANIFEST, Error.Code.DASH_INVALID_XML);
}
// Get any Location elements. This will update the manifest location and
// the base URI.
/** @type {!Array.<string>} */
var manifestBaseUris = [finalManifestUri];
/** @type {!Array.<string>} */
var locations = XmlUtils.findChildren(mpd, 'Location')
.map(XmlUtils.getContents)
.filter(Functional.isNotNull);
if (locations.length > 0) {
this.manifestUris_ = locations;
manifestBaseUris = locations;
}
var uris = XmlUtils.findChildren(mpd, 'BaseURL').map(XmlUtils.getContents);
var baseUris = shaka.dash.MpdUtils.resolveUris(manifestBaseUris, uris);
var minBufferTime =
XmlUtils.parseAttr(mpd, 'minBufferTime', XmlUtils.parseDuration);
this.updatePeriod_ = /** @type {number} */ (XmlUtils.parseAttr(
mpd, 'minimumUpdatePeriod', XmlUtils.parseDuration, -1));
var presentationStartTime = XmlUtils.parseAttr(
mpd, 'availabilityStartTime', XmlUtils.parseDate);
var segmentAvailabilityDuration = XmlUtils.parseAttr(
mpd, 'timeShiftBufferDepth', XmlUtils.parseDuration);
var suggestedPresentationDelay = XmlUtils.parseAttr(
mpd, 'suggestedPresentationDelay', XmlUtils.parseDuration);
var maxSegmentDuration = XmlUtils.parseAttr(
mpd, 'maxSegmentDuration', XmlUtils.parseDuration);
var mpdType = mpd.getAttribute('type') || 'static';
var presentationTimeline;
if (this.manifest_) {
presentationTimeline = this.manifest_.presentationTimeline;
} else {
// DASH IOP v3.0 suggests using a default delay between minBufferTime and
// timeShiftBufferDepth. This is literally the range of all feasible
// choices for the value. Nothing older than timeShiftBufferDepth is still
// available, and anything less than minBufferTime will cause buffering
// issues.
//
// We have decided that our default will be 1.5 * minBufferTime, or 10s,
// whichever is larger. This is fairly conservative. Content providers
// should provide a suggestedPresentationDelay whenever possible to optimize
// the live streaming experience.
var defaultPresentationDelay = Math.max(
shaka.dash.DashParser.DEFAULT_SUGGESTED_PRESENTATION_DELAY_,
minBufferTime * 1.5);
var presentationDelay = suggestedPresentationDelay != null ?
suggestedPresentationDelay : defaultPresentationDelay;
presentationTimeline = new shaka.media.PresentationTimeline(
presentationStartTime, presentationDelay);
}
/** @type {shaka.dash.DashParser.Context} */
var context = {
dynamic: mpdType != 'static',
presentationTimeline: presentationTimeline,
period: null,
periodInfo: null,
adaptationSet: null,
representation: null,
bandwidth: undefined,
indexRangeWarningGiven: false
};
var periodsAndDuration = this.parsePeriods_(context, baseUris, mpd);
var duration = periodsAndDuration.duration;
var periods = periodsAndDuration.periods;
presentationTimeline.setStatic(mpdType == 'static');
presentationTimeline.setDuration(duration || Infinity);
presentationTimeline.setSegmentAvailabilityDuration(
segmentAvailabilityDuration != null ?
segmentAvailabilityDuration :
Infinity);
// Use @maxSegmentDuration to override smaller, derived values.
presentationTimeline.notifyMaxSegmentDuration(maxSegmentDuration || 1);
if (!COMPILED) presentationTimeline.assertIsValid();
if (this.manifest_) {
// This is a manifest update, so we're done.
return Promise.resolve();
}
// This is the first manifest parse, so we cannot return until we calculate
// the clock offset.
var timingElements = XmlUtils.findChildren(mpd, 'UTCTiming');
var isLive = presentationTimeline.isLive();
// if any of the periods had an emsg box indicator,
// register a response filter to look for an EMSG box in segments
if (periodsAndDuration.containsInband)
this.networkingEngine_.registerResponseFilter(this.emsgResponseFilter_);
return this.parseUtcTiming_(
baseUris, timingElements, isLive).then(function(offset) {
// Detect calls to stop().
if (!this.networkingEngine_)
return;
presentationTimeline.setClockOffset(offset);
this.manifest_ = {
presentationTimeline: presentationTimeline,
periods: periods,
offlineSessionIds: [],
minBufferTime: minBufferTime || 0
};
}.bind(this));
};
/**
* Reads and parses the periods from the manifest. This first does some
* partial parsing so the start and duration is available when parsing children.
*
* @param {shaka.dash.DashParser.Context} context
* @param {!Array.<string>} baseUris
* @param {!Element} mpd
* @return {{periods: !Array.<shakaExtern.Period>,
* duration: ?number, containsInband: boolean}}
* @private
*/
shaka.dash.DashParser.prototype.parsePeriods_ = function(
context, baseUris, mpd) {
var Functional = shaka.util.Functional;
var XmlUtils = shaka.util.XmlUtils;
var presentationDuration = XmlUtils.parseAttr(
mpd, 'mediaPresentationDuration', XmlUtils.parseDuration);
var containsInband = false;
var periods = [];
var prevEnd = 0;
var periodNodes = XmlUtils.findChildren(mpd, 'Period');
for (var i = 0; i < periodNodes.length; i++) {
var elem = periodNodes[i];
var start = /** @type {number} */ (
XmlUtils.parseAttr(elem, 'start', XmlUtils.parseDuration, prevEnd));
var periodDuration =
XmlUtils.parseAttr(elem, 'duration', XmlUtils.parseDuration);
if (periodDuration == null) {
if (i + 1 != periodNodes.length) {
// "The difference between the start time of a Period and the start time
// of the following Period is the duration of the media content
// represented by this Period."
var nextPeriod = periodNodes[i + 1];
var nextStart =
XmlUtils.parseAttr(nextPeriod, 'start', XmlUtils.parseDuration);
if (nextStart != null)
periodDuration = nextStart - start;
} else if (presentationDuration != null) {
// "The Period extends until the Period.start of the next Period, or
// until the end of the Media Presentation in the case of the last
// Period."
periodDuration = presentationDuration - start;
}
}
// Parse child nodes.
var info = {
start: start,
duration: periodDuration,
node: elem,
containsInband: false
};
var period = this.parsePeriod_(context, baseUris, info);
periods.push(period);
// parsePeriod_ has now calculated info.containsInband.
containsInband = containsInband || info.containsInband;
// If there are any new periods, call the callback and add them to the
// manifest. If this is the first parse, it will see all of them as new.
var periodId = context.period.id;
if (this.periodIds_.every(Functional.isNotEqualFunc(periodId))) {
this.filterPeriod_(period);
this.periodIds_.push(periodId);
if (this.manifest_)
this.manifest_.periods.push(period);
}
if (periodDuration == null) {
if (i + 1 != periodNodes.length) {
// If the duration is still null and we aren't at the end, then we will
// skip any remaining periods.
shaka.log.warning(
'Skipping Period', i + 1, 'and any subsequent Periods:', 'Period',
i + 1, 'does not have a valid start time.', periods[i + 1]);
}
// The duration is unknown, so the end is unknown.
prevEnd = null;
break;
}
prevEnd = start + periodDuration;
}
if (presentationDuration != null) {
if (prevEnd != presentationDuration) {
shaka.log.warning(
'@mediaPresentationDuration does not match the total duration of all',
'Periods.');
// Assume @mediaPresentationDuration is correct.
}
return {
periods: periods,
duration: presentationDuration,
containsInband: containsInband
};
} else {
return {
periods: periods,
duration: prevEnd,
containsInband: containsInband
};
}
};
/**
* Parses a Period XML element. Unlike the other parse methods, this is not
* given the Node; it is given a PeriodInfo structure. Also, partial parsing
* was done before this was called so start and duration are valid.
*
* @param {shaka.dash.DashParser.Context} context
* @param {!Array.<string>} baseUris
* @param {shaka.dash.DashParser.PeriodInfo} periodInfo
* @return {shakaExtern.Period}
* @throws shaka.util.Error When there is a parsing error.
* @private
*/
shaka.dash.DashParser.prototype.parsePeriod_ = function(
context, baseUris, periodInfo) {
var Functional = shaka.util.Functional;
var XmlUtils = shaka.util.XmlUtils;
context.period = this.createFrame_(periodInfo.node, null, baseUris);
context.periodInfo = periodInfo;
// If the period doesn't have an ID, give it one based on its start time.
if (!context.period.id) {
shaka.log.info(
'No Period ID given for Period with start time ' + periodInfo.start +
', Assigning a default');
context.period.id = '__shaka_period_' + periodInfo.start;
}
var adaptationSetNodes =
XmlUtils.findChildren(periodInfo.node, 'AdaptationSet');
var adaptationSets = adaptationSetNodes
.map(this.parseAdaptationSet_.bind(this, context))
.filter(Functional.isNotNull);
var representationIds = adaptationSets
.map(function(as) { return as.representationIds; })
.reduce(Functional.collapseArrays, []);
var uniqueRepIds = representationIds.filter(Functional.isNotDuplicate);
if (representationIds.length != uniqueRepIds.length) {
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_DUPLICATE_REPRESENTATION_ID);
}
if (adaptationSets.length == 0) {
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_EMPTY_PERIOD);
}
// see if any adaptation set has emsg indicator on it.
// If it does, we'll register a response filter later.
for (var i = 0; i < adaptationSets.length; i++) {
if (adaptationSets[i].containsInband) {
periodInfo.containsInband = true;
}
}
var streamSets = this.createStreamSets_(adaptationSets);
return {startTime: periodInfo.start, streamSets: streamSets};
};
/**
* Parses an AdaptationSet XML element.
*
* @param {shaka.dash.DashParser.Context} context
* @param {!Element} elem The AdaptationSet element.
* @return {?shaka.dash.DashParser.AdaptationInfo}
* @throws shaka.util.Error When there is a parsing error.
* @private
*/
shaka.dash.DashParser.prototype.parseAdaptationSet_ = function(context, elem) {
var XmlUtils = shaka.util.XmlUtils;
context.adaptationSet = this.createFrame_(elem, context.period, null);
var main = false;
var roles = XmlUtils.findChildren(elem, 'Role');
// Default kind for text streams is 'subtitle' if unspecified in the manifest.
var kind = undefined;
if (context.adaptationSet.contentType == 'text') kind = 'subtitle';
for (var i = 0; i < roles.length; i++) {
var scheme = roles[i].getAttribute('schemeIdUri');
if (scheme == null || scheme == 'urn:mpeg:dash:role:2011') {
// These only apply for the given scheme, but allow them to be specified
// if there is no scheme specified.
// See: DASH section 5.8.5.5
var value = roles[i].getAttribute('value');
switch (value) {
case 'main':
main = true;
break;
case 'caption':
case 'subtitle':
kind = value;
break;
}
}
}
// InbandEventStream indicates that a segment contains inband
// information.
var containsInband = this.inBandEventStreamIsPresent_(elem);
var supplementalProperties =
XmlUtils.findChildren(elem, 'SupplementalProperty');
var switchableIds = [];
supplementalProperties.forEach(function(prop) {
var schemeId = prop.getAttribute('schemeIdUri');
if (schemeId == 'urn:mpeg:dash:adaptation-set-switching:2016' ||
schemeId == 'http://dashif.org/guidelines/AdaptationSetSwitching' ||
schemeId == 'http://dashif.org/descriptor/AdaptationSetSwitching') {
var value = prop.getAttribute('value');
if (value)
switchableIds.push.apply(switchableIds, value.split(','));
}
});
var essentialProperties = XmlUtils.findChildren(elem, 'EssentialProperty');
// ID of real AdaptationSet if this is a trick mode set:
var trickModeFor = null;
var unrecognizedEssentialProperty = false;
essentialProperties.forEach(function(prop) {
var schemeId = prop.getAttribute('schemeIdUri');
if (schemeId == 'http://dashif.org/guidelines/trickmode') {
trickModeFor = prop.getAttribute('value');
} else {
unrecognizedEssentialProperty = true;
}
});
if (trickModeFor != null) {
// Ignore trick mode tracks until we support them fully.
return null;
}
// According to DASH spec (2014) section 5.8.4.8, "the successful processing
// of the descriptor is essential to properly use the information in the
// parent element". According to DASH IOP v3.3, section 3.3.4, "if the scheme
// or the value" for EssentialProperty is not recognized, "the DASH client
// shall ignore the parent element."
if (unrecognizedEssentialProperty) {
// Stop parsing this AdaptationSet and let the caller filter out the nulls.
return null;
}
var contentProtectionElems = XmlUtils.findChildren(elem, 'ContentProtection');
var contentProtection = shaka.dash.ContentProtection.parseFromAdaptationSet(
contentProtectionElems, this.config_.dash.customScheme);
var language =
shaka.util.LanguageUtils.normalize(elem.getAttribute('lang') || 'und');
// Parse Representations into Streams.
var representations = XmlUtils.findChildren(elem, 'Representation');
var streams = representations
.map(this.parseRepresentation_.bind(
this, context, contentProtection, kind, language))
.filter(function(s) { return !!s; });
if (streams.length == 0) {
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_EMPTY_ADAPTATION_SET);
}
if (!context.adaptationSet.contentType) {
// Guess the AdaptationSet's content type.
var mimeType = streams[0].mimeType;
var codecs = streams[0].codecs;
context.adaptationSet.contentType =
shaka.dash.DashParser.guessContentType_(mimeType, codecs);
}
var repIds = representations
.map(function(node) { return node.getAttribute('id'); })
.filter(shaka.util.Functional.isNotNull);
return {
id: context.adaptationSet.id || ('__fake__' + this.globalId_++),
contentType: context.adaptationSet.contentType,
language: language,
main: main,
streams: streams,
drmInfos: contentProtection.drmInfos,
switchableIds: switchableIds,
containsInband: containsInband,
representationIds: repIds
};
};
/**
* Indicates whether an InbandEventStream element is present at the Adaption
* Set or Representation level.
*
* @param {!Element} elem The AdaptationSet element.
* @return {boolean} Whether the InbandEventStream element is present.
* @private
*/
shaka.dash.DashParser.prototype.inBandEventStreamIsPresent_ = function(elem) {
var XmlUtils = shaka.util.XmlUtils;
var adaptationEventStream = XmlUtils.findChild(elem, 'InbandEventStream');
if (adaptationEventStream != null) {
return true;
}
var representations = XmlUtils.findChildren(elem, 'Representation');
var representationEventStream = null;
if (representations.length > 0) {
for (var i = 0; i < representations.length; i++) {
representationEventStream =
XmlUtils.findChild(representations[i], 'InbandEventStream');
if (representationEventStream)
return true;
}
}
return false;
};
/**
* Parses a Representation XML element.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.ContentProtection.Context} contentProtection
* @param {(string|undefined)} kind
* @param {string} language
* @param {!Element} node
* @return {?shakaExtern.Stream} The Stream, or null when there is a
* non-critical parsing error.
* @throws shaka.util.Error When there is a parsing error.
* @private
*/
shaka.dash.DashParser.prototype.parseRepresentation_ = function(
context, contentProtection, kind, language, node) {
var XmlUtils = shaka.util.XmlUtils;
context.representation = this.createFrame_(node, context.adaptationSet, null);
if (!this.verifyRepresentation_(context.representation)) {
shaka.log.warning('Skipping Representation', context.representation);
return null;
}
context.bandwidth =
XmlUtils.parseAttr(node, 'bandwidth', XmlUtils.parsePositiveInt) ||
undefined;
/** @type {?shaka.dash.DashParser.StreamInfo} */
var streamInfo;
var requestInitSegment = this.requestInitSegment_.bind(this);
if (context.representation.segmentBase) {
streamInfo = shaka.dash.SegmentBase.createStream(
context, requestInitSegment);
} else if (context.representation.segmentList) {
streamInfo = shaka.dash.SegmentList.createStream(
context, this.segmentIndexMap_);
} else if (context.representation.segmentTemplate) {
streamInfo = shaka.dash.SegmentTemplate.createStream(
context, requestInitSegment, this.segmentIndexMap_, !!this.manifest_);
} else {
goog.asserts.assert(context.representation.contentType == 'text' ||
context.representation.contentType == 'application',
'Must have Segment* with non-text streams.');
var baseUris = context.representation.baseUris;
var duration = context.periodInfo.duration || 0;
streamInfo = {
createSegmentIndex: Promise.resolve.bind(Promise),
findSegmentPosition:
/** @return {?number} */ function(/** number */ time) {
if (time >= 0 && time < duration)
return 1;
else
return null;
},
getSegmentReference:
/** @return {shaka.media.SegmentReference} */
function(/** number */ ref) {
if (ref != 1)
return null;
return new shaka.media.SegmentReference(
1, 0, duration, function() { return baseUris; }, 0, null);
},
initSegmentReference: null,
presentationTimeOffset: 0
};
}
var contentProtectionElems = XmlUtils.findChildren(node, 'ContentProtection');
var keyId = shaka.dash.ContentProtection.parseFromRepresentation(
contentProtectionElems, this.config_.dash.customScheme,
contentProtection);
return {
id: this.globalId_++,
createSegmentIndex: streamInfo.createSegmentIndex,
findSegmentPosition: streamInfo.findSegmentPosition,
getSegmentReference: streamInfo.getSegmentReference,
initSegmentReference: streamInfo.initSegmentReference,
presentationTimeOffset: streamInfo.presentationTimeOffset,
mimeType: context.representation.mimeType,
codecs: context.representation.codecs,
frameRate: context.representation.frameRate,
bandwidth: context.bandwidth,
width: context.representation.width,
height: context.representation.height,
kind: kind,
encrypted: contentProtection.drmInfos.length > 0,
keyId: keyId,
language: language,
allowedByApplication: true,
allowedByKeySystem: true
};
};
/**
* Called when the update timer ticks.
*
* @private
*/
shaka.dash.DashParser.prototype.onUpdate_ = function() {
goog.asserts.assert(this.updateTimer_, 'Should only be called by timer');
goog.asserts.assert(this.updatePeriod_ >= 0,
'There should be an update period');
shaka.log.info('Updating manifest...');
this.updateTimer_ = null;
var startTime = Date.now();
this.requestManifest_().then(function() {
// Detect a call to stop()
if (!this.networkingEngine_)
return;
// Ensure the next update occurs within |updatePeriod_| seconds by taking
// into account the time it took to update the manifest.
var endTime = Date.now();
this.setUpdateTimer_((endTime - startTime) / 1000.0);
}.bind(this)).catch(function(error) {
goog.asserts.assert(error instanceof shaka.util.Error,
'Should only receive a Shaka error');
this.onError_(error);
// Try updating again, but ensure we haven't been destroyed.
if (this.networkingEngine_) {
this.setUpdateTimer_(0);
}
}.bind(this));
};
/**
* Sets the update timer. Does nothing if the manifest does not specify an
* update period.
*
* @param {number} offset An offset, in seconds, to apply to the manifest's
* update period.
* @private
*/
shaka.dash.DashParser.prototype.setUpdateTimer_ = function(offset) {
if (this.updatePeriod_ < 0)
return;
goog.asserts.assert(this.updateTimer_ == null,
'Timer should not be already set');
var period =
Math.max(shaka.dash.DashParser.MIN_UPDATE_PERIOD_, this.updatePeriod_);
var interval = Math.max(period - offset, 0);
shaka.log.debug('updateInterval', interval);
var callback = this.onUpdate_.bind(this);
this.updateTimer_ = window.setTimeout(callback, 1000 * interval);
};
/**
* Creates a new inheritance frame for the given element.
*
* @param {!Element} elem
* @param {?shaka.dash.DashParser.InheritanceFrame} parent
* @param {Array.<string>} baseUris
* @return {shaka.dash.DashParser.InheritanceFrame}
* @private
*/
shaka.dash.DashParser.prototype.createFrame_ = function(
elem, parent, baseUris) {
goog.asserts.assert(parent || baseUris,
'Must provide either parent or baseUris');
var MpdUtils = shaka.dash.MpdUtils;
var XmlUtils = shaka.util.XmlUtils;
parent = parent || /** @type {shaka.dash.DashParser.InheritanceFrame} */ ({
contentType: '',
mimeType: '',
codecs: '',
frameRate: undefined
});
baseUris = baseUris || parent.baseUris;
var parseNumber = XmlUtils.parseNonNegativeInt;
var evalDivision = XmlUtils.evalDivision;
var uris = XmlUtils.findChildren(elem, 'BaseURL').map(XmlUtils.getContents);
var contentType = elem.getAttribute('contentType') || parent.contentType;
var mimeType = elem.getAttribute('mimeType') || parent.mimeType;
var codecs = elem.getAttribute('codecs') || parent.codecs;
var frameRate = XmlUtils.parseAttr(elem, 'frameRate',
evalDivision) || parent.frameRate;
if (!contentType) {
contentType = shaka.dash.DashParser.guessContentType_(mimeType, codecs);
}
return {
baseUris: MpdUtils.resolveUris(baseUris, uris),
segmentBase: XmlUtils.findChild(elem, 'SegmentBase') || parent.segmentBase,
segmentList: XmlUtils.findChild(elem, 'SegmentList') || parent.segmentList,
segmentTemplate:
XmlUtils.findChild(elem, 'SegmentTemplate') || parent.segmentTemplate,
width: XmlUtils.parseAttr(elem, 'width', parseNumber) || parent.width,
height: XmlUtils.parseAttr(elem, 'height', parseNumber) || parent.height,
contentType: contentType,
mimeType: mimeType,
codecs: codecs,
frameRate: frameRate,
id: elem.getAttribute('id')
};
};
/**
* Creates the StreamSet objects for the given AdaptationSets. This will group
* stream sets according to which streams it can switch to. If AdaptationSet
* A can switch to B, it is assumed that B can switch to A (as well as any
* stream that A can switch to).
*
* @param {!Array.<shaka.dash.DashParser.AdaptationInfo>} adaptationSets
* @return {!Array.<shakaExtern.StreamSet>}
* @private
*/
shaka.dash.DashParser.prototype.createStreamSets_ = function(adaptationSets) {
var Functional = shaka.util.Functional;
/**
* A map of ID to the group it belongs to. Multiple IDs can map to the same
* group. Each entry in the group will map back to the same array.
* @type {!Object.<string, !Array.<shaka.dash.DashParser.AdaptationInfo>>}
*/
var groupMap = {};
// Create an initial map of all AS.
adaptationSets.forEach(function(set) { groupMap[set.id] = [set]; });
// Merge any AdaptationSets that can switch to each other.
adaptationSets.forEach(function(set) {
var group = groupMap[set.id];
set.switchableIds.forEach(function(id) {
var otherGroup = groupMap[id];
if (!otherGroup || otherGroup == group)
return;
// Merge the other group into the new one.
group.push.apply(group, otherGroup);
// Update each ID of the old group to map to the new group.
otherGroup.forEach(function(other) {
groupMap[other.id] = group;
});
});
});
/** @type {!Array.<shakaExtern.StreamSet>} */
var ret = [];
/** @type {!Array.<!Array.<shaka.dash.DashParser.AdaptationInfo>>} */
var seenGroups = [];
shaka.util.MapUtils.values(groupMap).forEach(function(group) {
if (seenGroups.indexOf(group) >= 0)
return;
seenGroups.push(group);
// First group AdaptationSets by type.
var setsByType = new shaka.util.MultiMap();
group.forEach(function(set) {
setsByType.push(set.contentType || '', set);
});
setsByType.keys().forEach(function(type) {
// Finally group AdaptationSets of the same type and group by language,
// then squash them into the same StreamSetInfo.
var setsByLang = new shaka.util.MultiMap();
setsByType.get(type).forEach(function(set) {
setsByLang.push(set.language, set);
});
setsByLang.keys().forEach(function(lang) {
var sets =
/** @type {!Array.<shaka.dash.DashParser.AdaptationInfo>} */ (
setsByLang.get(lang));
/** @type {shakaExtern.StreamSet} */
var streamSet = {
language: lang,
type: type,
primary: sets.some(function(s) { return s.main; }),
drmInfos:
sets.map(function(s) { return s.drmInfos; })
.reduce(Functional.collapseArrays, []),
streams:
sets.map(function(s) { return s.streams; })
.reduce(Functional.collapseArrays, [])
};
ret.push(streamSet);
}); // forEach lang
}); // forEach type
}); // map groupId
return ret;
};
/**
* Verifies that a Representation has exactly one Segment* element. Prints
* warnings if there is a problem.
*
* @param {shaka.dash.DashParser.InheritanceFrame} frame
* @return {boolean} True if the Representation is usable; otherwise return
* false.
* @private
*/
shaka.dash.DashParser.prototype.verifyRepresentation_ = function(frame) {
var n = 0;
n += frame.segmentBase ? 1 : 0;
n += frame.segmentList ? 1 : 0;
n += frame.segmentTemplate ? 1 : 0;
if (n == 0) {
// TODO: extend with the list of MIME types registered to TextEngine.
if (frame.contentType == 'text' || frame.contentType == 'application') {
return true;
} else {
shaka.log.warning(
'Representation does not contain a segment information source:',
'the Representation must contain one of SegmentBase, SegmentList,',
'SegmentTemplate, or explicitly indicate that it is "text".',
frame);
return false;
}
}
if (n != 1) {
shaka.log.warning(
'Representation contains multiple segment information sources:',
'the Representation should only contain one of SegmentBase,',
'SegmentList, or SegmentTemplate.',
frame);
if (frame.segmentBase) {
shaka.log.info('Using SegmentBase by default.');
frame.segmentList = null;
frame.segmentTemplate = null;
} else {
goog.asserts.assert(frame.segmentList, 'There should be a SegmentList');
shaka.log.info('Using SegmentList by default.');
frame.segmentTemplate = null;
}
}
return true;
};
/**
* Makes a request to the given URI and calculates the clock offset.
*
* @param {!Array.<string>} baseUris
* @param {string} uri
* @param {string} method
* @return {!Promise.<number>}
* @private
*/
shaka.dash.DashParser.prototype.requestForTiming_ =
function(baseUris, uri, method) {
var requestUris = shaka.dash.MpdUtils.resolveUris(baseUris, [uri]);
var request = shaka.net.NetworkingEngine.makeRequest(
requestUris, this.config_.retryParameters);
request.method = method;
var type = shaka.net.NetworkingEngine.RequestType.MANIFEST;
return this.networkingEngine_.request(type, request).then(function(response) {
var text;
if (method == 'HEAD') {
if (!response.headers || !response.headers['date'])
return 0;
text = response.headers['date'];
} else {
text = shaka.util.StringUtils.fromUTF8(response.data);
}
var date = Date.parse(text);
return isNaN(date) ? 0 : (date - Date.now());
});
};
/**
* Parses an array of UTCTiming elements.
*
* @param {!Array.<string>} baseUris
* @param {!Array.<!Element>} elems
* @param {boolean} isLive
* @return {!Promise.<number>}
* @private
*/
shaka.dash.DashParser.prototype.parseUtcTiming_ =
function(baseUris, elems, isLive) {
var schemesAndValues = elems.map(function(elem) {
return {
scheme: elem.getAttribute('schemeIdUri'),
value: elem.getAttribute('value')
};
});
// If there's nothing specified in the manifest, but we have a default from
// the config, use that.
var clockSyncUri = this.config_.dash.clockSyncUri;
if (isLive && !schemesAndValues.length && clockSyncUri) {
schemesAndValues.push({
scheme: 'urn:mpeg:dash:utc:http-head:2014',
value: clockSyncUri
});
}
var Functional = shaka.util.Functional;
return Functional.createFallbackPromiseChain(schemesAndValues, function(sv) {
var scheme = sv.scheme;
var value = sv.value;
switch (scheme) {
// See DASH IOP Guidelines Section 4.7
// http://goo.gl/CQFNJT
case 'urn:mpeg:dash:utc:http-head:2014':
// Some old ISO23009-1 drafts used 2012.
case 'urn:mpeg:dash:utc:http-head:2012':
return this.requestForTiming_(baseUris, value, 'HEAD');
case 'urn:mpeg:dash:utc:http-xsdate:2014':
case 'urn:mpeg:dash:utc:http-iso:2014':
case 'urn:mpeg:dash:utc:http-xsdate:2012':
case 'urn:mpeg:dash:utc:http-iso:2012':
return this.requestForTiming_(baseUris, value, 'GET');
case 'urn:mpeg:dash:utc:direct:2014':
case 'urn:mpeg:dash:utc:direct:2012':
var date = Date.parse(value);
return isNaN(date) ? 0 : (date - Date.now());
case 'urn:mpeg:dash:utc:http-ntp:2014':
case 'urn:mpeg:dash:utc:ntp:2014':
case 'urn:mpeg:dash:utc:sntp:2014':
shaka.log.warning('NTP UTCTiming scheme is not supported');
return Promise.reject();
default:
shaka.log.warning(
'Unrecognized scheme in UTCTiming element', scheme);
return Promise.reject();
}
}.bind(this)).catch(function() {
if (isLive) {
shaka.log.warning(
'A UTCTiming element should always be given in live manifests! ' +
'This content may not play on clients with bad clocks!');
}
return 0;
});
};
/**
* Makes a network request on behalf of SegmentBase.createStream.
*
* @param {!Array.<string>} uris
* @param {?number} startByte
* @param {?number} endByte
* @return {!Promise.<!ArrayBuffer>}
* @private
*/
shaka.dash.DashParser.prototype.requestInitSegment_ = function(
uris, startByte, endByte) {
var requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
var request = shaka.net.NetworkingEngine.makeRequest(
uris, this.config_.retryParameters);
if (startByte != null) {
var end = (endByte != null ? endByte : '');
request.headers['Range'] = 'bytes=' + startByte + '-' + end;
}
return this.networkingEngine_.request(requestType, request)
.then(function(response) { return response.data; });
};
/**
* Response filter that looks for presence of EMSG
* boxes in segments. If a box is found, depending on the content it
* either triggers the manifest update or dispatches an event with the
* box content to the application.
*
* @param {shaka.net.NetworkingEngine.RequestType} type
* @param {!shakaExtern.Response} response
* @private
*/
shaka.dash.DashParser.prototype.emsgResponseFilter_ = function(type, response) {
// Only look for segment responses:
if (type == shaka.net.NetworkingEngine.RequestType.SEGMENT) {
var reader = new shaka.util.DataViewReader(new DataView(response.data),
shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
var boxSize = shaka.util.Mp4Parser.findBox(
shaka.dash.DashParser.BOX_TYPE_EMSG, reader);
if (boxSize != shaka.util.Mp4Parser.BOX_NOT_FOUND) {
var start = reader.getPosition() - 8;
var end = start + boxSize;
// skip version and flags
reader.skip(4);
var scheme_id = reader.readTerminatedString();
// scheme_id of "urn:mpeg:dash:event:2012" means it's
// time to update the manifest
if (scheme_id == shaka.dash.DashParser.DASH_EMSG_SCHEME_ID_URI) {
// trigger manifest update
this.requestManifest_();
} else {
// read rest of the data and dispatch event to the application
var value = reader.readTerminatedString();
var timescale = reader.readUint32();
var presentationTimeDelta = reader.readUint32();
var eventDuration = reader.readUint32();
var id = reader.readUint32();
var messageData = reader.readBytes(end - reader.getPosition());
/** @type {shakaExtern.EmsgInfo} */
var emsg = {
schemeIdUri: scheme_id,
value: value,
timescale: timescale,
presentationTimeDelta: presentationTimeDelta,
eventDuration: eventDuration,
id: id,
messageData: messageData
};
var event = new shaka.util.FakeEvent(
'emsg', { 'detail': emsg });
this.onEvent_(event);
}
}
}
};
/**
* Guess the content type based on MIME type and codecs.
*
* @param {string} mimeType
* @param {string} codecs
* @return {string}
* @private
*/
shaka.dash.DashParser.guessContentType_ = function(mimeType, codecs) {
var fullMimeType = shaka.util.StreamUtils.getFullMimeType(mimeType, codecs);
if (shaka.media.TextEngine.isTypeSupported(fullMimeType)) {
// If it's supported by TextEngine, it's definitely text.
// We don't check MediaSourceEngine, because that would report support
// for platform-supported video and audio types as well.
return 'text';
}
// Otherwise, just split the MIME type. This handles video and audio
// types well.
return mimeType.split('/')[0];
};
/** @const {number} */
shaka.dash.DashParser.BOX_TYPE_EMSG = 0x656D7367;
/** @const {string} */
shaka.dash.DashParser.DASH_EMSG_SCHEME_ID_URI = 'urn:mpeg:dash:event:2012';
shaka.media.ManifestParser.registerParserByExtension(
'mpd', shaka.dash.DashParser);
shaka.media.ManifestParser.registerParserByMime(
'application/dash+xml', shaka.dash.DashParser);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.dash.MpdUtils');
goog.require('goog.Uri');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.util.Functional');
goog.require('shaka.util.XmlUtils');
/**
* @namespace shaka.dash.MpdUtils
* @summary MPD processing utility functions.
*/
/**
* Specifies how tolerant the player is to inaccurate segment start times and
* end times within a manifest. For example, gaps or overlaps between segments
* in a SegmentTimeline which are greater than or equal to this value will
* result in a warning message.
*
* @const {number}
*/
shaka.dash.MpdUtils.GAP_OVERLAP_TOLERANCE_SECONDS = 1 / 15;
/**
* @typedef {{
* start: number,
* unscaledStart: number,
* end: number
* }}
*
* @description
* Defines a time range of a media segment. Times are in seconds.
*
* @property {number} start
* The start time of the range.
* @property {number} unscaledStart
* The start time of the range in representation timescale units.
* @property {number} end
* The end time (exclusive) of the range.
*/
shaka.dash.MpdUtils.TimeRange;
/**
* @typedef {{
* timescale: number,
* segmentDuration: ?number,
* startNumber: number,
* presentationTimeOffset: number,
* unscaledPresentationTimeOffset: number,
* timeline: Array.<shaka.dash.MpdUtils.TimeRange>
* }}
*
* @description
* Contains common information between SegmentList and SegmentTemplate items.
*
* @property {number} timescale
* The time-scale of the representation.
* @property {?number} segmentDuration
* The duration of the segments in seconds, if given.
* @property {number} startNumber
* The start number of the segments; 1 or greater.
* @property {number} presentationTimeOffset
* The presentationTimeOffset of the representation, in seconds.
* @property {number} unscaledPresentationTimeOffset
* The presentationTimeOffset of the representation, in timescale units.
* @property {Array.<shaka.dash.MpdUtils.TimeRange>} timeline
* The timeline of the representation, if given. Times in seconds.
*/
shaka.dash.MpdUtils.SegmentInfo;
/**
* Fills a SegmentTemplate URI template. This function does not validate the
* resulting URI.
*
* @param {string} uriTemplate
* @param {?string} representationId
* @param {?number} number
* @param {?number} bandwidth
* @param {?number} time
* @return {string} A URI string.
* @see ISO/IEC 23009-1:2014 section 5.3.9.4.4
*/
shaka.dash.MpdUtils.fillUriTemplate = function(
uriTemplate, representationId, number, bandwidth, time) {
if (time !== null) {
goog.asserts.assert(Math.abs(time - Math.round(time)) < 0.2,
'Calculated $Time$ values must be close to integers!');
time = Math.round(time);
}
/** @type {!Object.<string, ?number|?string>} */
var valueTable = {
'RepresentationID': representationId,
'Number': number,
'Bandwidth': bandwidth,
'Time': time
};
var re = /\$(RepresentationID|Number|Bandwidth|Time)?(?:%0([0-9]+)d)?\$/g;
var uri = uriTemplate.replace(re, function(match, name, widthString) {
if (match == '$$') {
return '$';
}
var value = valueTable[name];
goog.asserts.assert(value !== undefined, 'Unrecognized identifier');
// Note that |value| may be 0 or ''.
if (value == null) {
shaka.log.warning(
'URL template does not have an available substitution for identifier',
'"' + name + '":',
uriTemplate);
return match;
}
if (name == 'RepresentationID' && widthString) {
shaka.log.warning(
'URL template should not contain a width specifier for identifier',
'"RepresentationID":',
uriTemplate);
widthString = undefined;
}
var valueString = value.toString();
// Create padding string.
var width = window.parseInt(widthString, 10) || 1;
var paddingSize = Math.max(0, width - valueString.length);
var padding = (new Array(paddingSize + 1)).join('0');
return padding + valueString;
});
return uri;
};
/**
* Expands a SegmentTimeline into an array-based timeline. The results are in
* seconds.
*
* @param {!Element} segmentTimeline
* @param {number} timescale
* @param {number} presentationTimeOffset
* @param {number} periodDuration The Period's duration in seconds.
* Infinity indicates that the Period continues indefinitely.
* @return {!Array.<shaka.dash.MpdUtils.TimeRange>}
*/
shaka.dash.MpdUtils.createTimeline = function(
segmentTimeline, timescale, presentationTimeOffset, periodDuration) {
goog.asserts.assert(
timescale > 0 && timescale < Infinity,
'timescale must be a positive, finite integer');
goog.asserts.assert(periodDuration > 0,
'period duration must be a positive integer');
// Alias.
var XmlUtils = shaka.util.XmlUtils;
var timePoints = XmlUtils.findChildren(segmentTimeline, 'S');
/** @type {!Array.<shaka.dash.MpdUtils.TimeRange>} */
var timeline = [];
var lastEndTime = 0;
for (var i = 0; i < timePoints.length; ++i) {
var timePoint = timePoints[i];
var t = XmlUtils.parseAttr(timePoint, 't', XmlUtils.parseNonNegativeInt);
var d = XmlUtils.parseAttr(timePoint, 'd', XmlUtils.parseNonNegativeInt);
var r = XmlUtils.parseAttr(timePoint, 'r', XmlUtils.parseInt);
// Adjust start considering the presentation time offset
if (t != null)
t -= presentationTimeOffset;
if (!d) {
shaka.log.warning(
'"S" element must have a duration:',
'ignoring the remaining "S" elements.',
timePoint);
return timeline;
}
var startTime = t != null ? t : lastEndTime;
var repeat = r || 0;
if (repeat < 0) {
if (i + 1 < timePoints.length) {
var nextTimePoint = timePoints[i + 1];
var nextStartTime = XmlUtils.parseAttr(
nextTimePoint, 't', XmlUtils.parseNonNegativeInt);
if (nextStartTime == null) {
shaka.log.warning(
'"S" element cannot have a negative repeat',
'if the next "S" element does not have a valid start time:',
'ignoring the remaining "S" elements.',
timePoint);
return timeline;
} else if (startTime >= nextStartTime) {
shaka.log.warning(
'"S" element cannot have a negative repeat',
'if its start time exceeds the next "S" element\'s start time:',
'ignoring the remaining "S" elements.',
timePoint);
return timeline;
}
repeat = Math.ceil((nextStartTime - startTime) / d) - 1;
} else {
if (periodDuration == Infinity) {
// The DASH spec. actually allows the last "S" element to have a
// negative repeat value even when the Period has an infinite
// duration. No one uses this feature and no one ever should, ever.
shaka.log.warning(
'The last "S" element cannot have a negative repeat',
'if the Period has an infinite duration:',
'ignoring the last "S" element.',
timePoint);
return timeline;
} else if (startTime / timescale >= periodDuration) {
shaka.log.warning(
'The last "S" element cannot have a negative repeat',
'if its start time exceeds the Period\'s duration:',
'igoring the last "S" element.',
timePoint);
return timeline;
}
repeat = Math.ceil((periodDuration * timescale - startTime) / d) - 1;
}
}
// The end of the last segment may end before the start of the current
// segment (a gap) or may end after the start of the current segment (an
// overlap). If there is a gap/overlap then stretch/compress the end of
// the last segment to the start of the current segment.
//
// Note: it is possible to move the start of the current segment to the
// end of the last segment, but this would complicate the computation of
// the $Time$ placeholder later on.
if ((timeline.length > 0) && (startTime != lastEndTime)) {
var delta = startTime - lastEndTime;
if (Math.abs(delta / timescale) >=
shaka.dash.MpdUtils.GAP_OVERLAP_TOLERANCE_SECONDS) {
shaka.log.warning(
'SegmentTimeline contains a large gap/overlap:',
'the content may have errors in it.',
timePoint);
}
timeline[timeline.length - 1].end = startTime / timescale;
}
for (var j = 0; j <= repeat; ++j) {
var endTime = startTime + d;
var item = {
start: startTime / timescale,
end: endTime / timescale,
unscaledStart: startTime
};
timeline.push(item);
startTime = endTime;
lastEndTime = endTime;
}
}
return timeline;
};
/**
* Expands the first SegmentReference so it begins at the start of its Period
* if it already begins close to the start of its Period, and expands or
* contracts the last SegmentReference so it ends at the end of its Period for
* VOD presentations.
*
* @param {boolean} dynamic
* @param {?number} periodDuration
* @param {!Array.<!shaka.media.SegmentReference>} references
*/
shaka.dash.MpdUtils.fitSegmentReferences = function(
dynamic, periodDuration, references) {
if (references.length == 0)
return;
/** @const {number} */
var tolerance = shaka.dash.MpdUtils.GAP_OVERLAP_TOLERANCE_SECONDS;
var firstReference = references[0];
if (firstReference.startTime <= tolerance) {
// Note: if the segment actually starts past 0, the video element should
// automatically jump the gap since the gap is small.
references[0] =
new shaka.media.SegmentReference(
firstReference.position,
0, firstReference.endTime,
firstReference.getUris,
firstReference.startByte, firstReference.endByte);
}
if (dynamic)
return;
goog.asserts.assert(periodDuration != null,
'Period duration must be known for static content!');
goog.asserts.assert(periodDuration != Infinity,
'Period duration must be finite for static content!');
var lastReference = references[references.length - 1];
// Sanity check.
goog.asserts.assert(
lastReference.startTime < periodDuration,
'lastReference cannot begin after the end of the Period');
if (lastReference.startTime > periodDuration) return;
// Log warning if necessary.
if (lastReference.endTime <= periodDuration - tolerance) {
shaka.log.warning(
'The last segment should not end before the end of the Period.',
lastReference);
} else if (lastReference.endTime >= periodDuration + tolerance) {
shaka.log.warning(
'The last segment should not end after the end of the Period.',
lastReference);
}
// Adjust the last SegmentReference.
references[references.length - 1] =
new shaka.media.SegmentReference(
lastReference.position,
lastReference.startTime, periodDuration,
lastReference.getUris,
lastReference.startByte, lastReference.endByte);
};
/**
* Resolves an array of relative URIs to the given base URIs. This will result
* in M*N number of URIs.
*
* @param {!Array.<string>} baseUris
* @param {!Array.<string>} relativeUris
* @return {!Array.<string>}
*/
shaka.dash.MpdUtils.resolveUris = function(baseUris, relativeUris) {
var Functional = shaka.util.Functional;
if (relativeUris.length == 0)
return baseUris;
var relativeAsGoog =
relativeUris.map(function(uri) { return new goog.Uri(uri); });
// Resolve each URI relative to each base URI, creating an Array of Arrays.
// Then flatten the Arrays into a single Array.
return baseUris.map(function(uri) { return new goog.Uri(uri); })
.map(function(base) { return relativeAsGoog.map(base.resolve.bind(base)); })
.reduce(Functional.collapseArrays, [])
.map(function(uri) { return uri.toString(); });
};
/**
* Parses common segment info for SegmentList and SegmentTemplate.
*
* @param {shaka.dash.DashParser.Context} context
* @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
* Gets the element that contains the segment info.
* @return {shaka.dash.MpdUtils.SegmentInfo}
*/
shaka.dash.MpdUtils.parseSegmentInfo = function(context, callback) {
goog.asserts.assert(
callback(context.representation),
'There must be at least one element of the given type.');
var MpdUtils = shaka.dash.MpdUtils;
var XmlUtils = shaka.util.XmlUtils;
var timescaleStr = MpdUtils.inheritAttribute(context, callback, 'timescale');
var timescale = 1;
if (timescaleStr) {
timescale = XmlUtils.parsePositiveInt(timescaleStr) || 1;
}
var durationStr = MpdUtils.inheritAttribute(context, callback, 'duration');
var segmentDuration = XmlUtils.parsePositiveInt(durationStr || '');
if (segmentDuration) {
segmentDuration /= timescale;
}
var startNumberStr =
MpdUtils.inheritAttribute(context, callback, 'startNumber');
var presentationTimeOffset =
MpdUtils.inheritAttribute(context, callback, 'presentationTimeOffset');
var startNumber = XmlUtils.parseNonNegativeInt(startNumberStr || '');
if (startNumberStr == null || startNumber == null)
startNumber = 1;
var timelineNode =
MpdUtils.inheritChild(context, callback, 'SegmentTimeline');
/** @type {Array.<shaka.dash.MpdUtils.TimeRange>} */
var timeline = null;
if (timelineNode) {
timeline = MpdUtils.createTimeline(
timelineNode, timescale, Number(presentationTimeOffset),
context.periodInfo.duration || Infinity);
}
var pto = (Number(presentationTimeOffset) / timescale) || 0;
return {
timescale: timescale,
segmentDuration: segmentDuration,
startNumber: startNumber,
presentationTimeOffset: pto,
unscaledPresentationTimeOffset: Number(presentationTimeOffset),
timeline: timeline
};
};
/**
* Searches the inheritance for a Segment* with the given attribute.
*
* @param {shaka.dash.DashParser.Context} context
* @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
* Gets the Element that contains the attribute to inherit.
* @param {string} attribute
* @return {?string}
*/
shaka.dash.MpdUtils.inheritAttribute = function(context, callback, attribute) {
var Functional = shaka.util.Functional;
goog.asserts.assert(
callback(context.representation),
'There must be at least one element of the given type');
/** @type {!Array.<!Element>} */
var nodes = [
callback(context.representation),
callback(context.adaptationSet),
callback(context.period)
].filter(Functional.isNotNull);
return nodes
.map(function(s) { return s.getAttribute(attribute); })
.reduce(function(all, part) { return all || part; });
};
/**
* Searches the inheritance for a Segment* with the given child.
*
* @param {shaka.dash.DashParser.Context} context
* @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
* Gets the Element that contains the child to inherit.
* @param {string} child
* @return {Element}
*/
shaka.dash.MpdUtils.inheritChild = function(context, callback, child) {
var Functional = shaka.util.Functional;
goog.asserts.assert(
callback(context.representation),
'There must be at least one element of the given type');
/** @type {!Array.<!Element>} */
var nodes = [
callback(context.representation),
callback(context.adaptationSet),
callback(context.period)
].filter(Functional.isNotNull);
var XmlUtils = shaka.util.XmlUtils;
return nodes
.map(function(s) { return XmlUtils.findChild(s, child); })
.reduce(function(all, part) { return all || part; });
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.dash.SegmentBase');
goog.require('goog.asserts');
goog.require('shaka.dash.MpdUtils');
goog.require('shaka.log');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.Mp4SegmentIndexParser');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.WebmSegmentIndexParser');
goog.require('shaka.util.Error');
goog.require('shaka.util.XmlUtils');
/**
* @namespace shaka.dash.SegmentBase
* @summary A set of functions for parsing SegmentBase elements.
*/
/**
* Creates an init segment reference from a Context object.
*
* @param {shaka.dash.DashParser.Context} context
* @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
* @return {shaka.media.InitSegmentReference}
*/
shaka.dash.SegmentBase.createInitSegment = function(context, callback) {
var MpdUtils = shaka.dash.MpdUtils;
var XmlUtils = shaka.util.XmlUtils;
var initialization =
MpdUtils.inheritChild(context, callback, 'Initialization');
if (!initialization)
return null;
var resolvedUris = context.representation.baseUris;
var uri = initialization.getAttribute('sourceURL');
if (uri) {
resolvedUris =
MpdUtils.resolveUris(context.representation.baseUris, [uri]);
}
var startByte = 0;
var endByte = null;
var range = XmlUtils.parseAttr(initialization, 'range', XmlUtils.parseRange);
if (range) {
startByte = range.start;
endByte = range.end;
}
var getUris = function() { return resolvedUris; };
return new shaka.media.InitSegmentReference(getUris, startByte, endByte);
};
/**
* Creates a new Stream object.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.DashParser.RequestInitSegmentCallback} requestInitSegment
* @throws shaka.util.Error When there is a parsing error.
* @return {shaka.dash.DashParser.StreamInfo}
*/
shaka.dash.SegmentBase.createStream = function(context, requestInitSegment) {
goog.asserts.assert(context.representation.segmentBase,
'Should only be called with SegmentBase');
// Since SegmentBase does not need updates, simply treat any call as
// the initial parse.
var MpdUtils = shaka.dash.MpdUtils;
var SegmentBase = shaka.dash.SegmentBase;
var presentationTimeOffset = MpdUtils.inheritAttribute(
context, SegmentBase.fromInheritance_, 'presentationTimeOffset');
var init =
SegmentBase.createInitSegment(context, SegmentBase.fromInheritance_);
var index = SegmentBase.createSegmentIndex_(
context, requestInitSegment, init, Number(presentationTimeOffset));
return {
createSegmentIndex: index.createSegmentIndex,
findSegmentPosition: index.findSegmentPosition,
getSegmentReference: index.getSegmentReference,
initSegmentReference: init,
presentationTimeOffset: Number(presentationTimeOffset) || 0
};
};
/**
* Creates segment index info for the given info.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.DashParser.RequestInitSegmentCallback} requestInitSegment
* @param {shaka.media.InitSegmentReference} init
* @param {!Array.<string>} uris
* @param {number} startByte
* @param {?number} endByte
* @param {string} containerType
* @param {number} presentationTimeOffset
* @return {shaka.dash.DashParser.SegmentIndexFunctions}
*/
shaka.dash.SegmentBase.createSegmentIndexFromUris = function(
context, requestInitSegment, init, uris,
startByte, endByte, containerType, presentationTimeOffset) {
var presentationTimeline = context.presentationTimeline;
var periodStartTime = context.periodInfo.start;
var periodDuration = context.periodInfo.duration;
// Create a local variable to bind to so we can set to null to help the GC.
var localRequest = requestInitSegment;
var segmentIndex = null;
var create = function() {
var async = [
localRequest(uris, startByte, endByte),
containerType == 'webm' ?
localRequest(init.getUris(), init.startByte, init.endByte) :
null
];
localRequest = null;
return Promise.all(async).then(function(results) {
var indexData = results[0];
var initData = results[1] || null;
var references = null;
if (containerType == 'mp4') {
references = shaka.media.Mp4SegmentIndexParser(
indexData, startByte, uris, presentationTimeOffset);
} else {
goog.asserts.assert(initData, 'WebM requires init data');
var parser = new shaka.media.WebmSegmentIndexParser();
references = parser.parse(indexData, initData, uris,
presentationTimeOffset);
}
shaka.dash.MpdUtils.fitSegmentReferences(
context.dynamic, periodDuration, references);
presentationTimeline.notifySegments(periodStartTime, references);
// Since containers are never updated, we don't need to store the
// segmentIndex in the map.
goog.asserts.assert(!segmentIndex,
'Should not call createSegmentIndex twice');
segmentIndex = new shaka.media.SegmentIndex(references);
});
};
var get = function(i) {
goog.asserts.assert(segmentIndex, 'Must call createSegmentIndex first');
return segmentIndex.get(i);
};
var find = function(t) {
goog.asserts.assert(segmentIndex, 'Must call createSegmentIndex first');
return segmentIndex.find(t);
};
return {
createSegmentIndex: create,
findSegmentPosition: find,
getSegmentReference: get
};
};
/**
* @param {?shaka.dash.DashParser.InheritanceFrame} frame
* @return {Element}
* @private
*/
shaka.dash.SegmentBase.fromInheritance_ = function(frame) {
return frame.segmentBase;
};
/**
* Creates segment index info from a Context object.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.DashParser.RequestInitSegmentCallback} requestInitSegment
* @param {shaka.media.InitSegmentReference} init
* @param {number} presentationTimeOffset
* @return {shaka.dash.DashParser.SegmentIndexFunctions}
* @throws shaka.util.Error When there is a parsing error.
* @private
*/
shaka.dash.SegmentBase.createSegmentIndex_ = function(
context, requestInitSegment, init, presentationTimeOffset) {
var MpdUtils = shaka.dash.MpdUtils;
var SegmentBase = shaka.dash.SegmentBase;
var XmlUtils = shaka.util.XmlUtils;
var contentType = context.representation.contentType;
var containerType = context.representation.mimeType.split('/')[1];
if (contentType != 'text' && containerType != 'mp4' &&
containerType != 'webm') {
shaka.log.error(
'SegmentBase specifies an unsupported container type.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_UNSUPPORTED_CONTAINER);
}
if ((containerType == 'webm') && !init) {
shaka.log.error(
'SegmentBase does not contain sufficient segment information:',
'the SegmentBase uses a WebM container,',
'but does not contain an Initialization element.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_WEBM_MISSING_INIT);
}
var representationIndex = MpdUtils.inheritChild(
context, SegmentBase.fromInheritance_, 'RepresentationIndex');
var indexRangeElem = MpdUtils.inheritAttribute(
context, SegmentBase.fromInheritance_, 'indexRange');
var indexUris = context.representation.baseUris;
var indexRange = XmlUtils.parseRange(indexRangeElem || '');
if (representationIndex) {
var representationUri = representationIndex.getAttribute('sourceURL');
if (representationUri) {
indexUris = MpdUtils.resolveUris(
context.representation.baseUris, [representationUri]);
}
indexRange = XmlUtils.parseAttr(
representationIndex, 'range', XmlUtils.parseRange, indexRange);
}
if (!indexRange) {
shaka.log.error(
'SegmentBase does not contain sufficient segment information:',
'the SegmentBase does not contain @indexRange',
'or a RepresentationIndex element.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
}
return shaka.dash.SegmentBase.createSegmentIndexFromUris(
context, requestInitSegment, init, indexUris, indexRange.start,
indexRange.end, containerType, presentationTimeOffset);
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.dash.SegmentList');
goog.require('goog.asserts');
goog.require('shaka.dash.MpdUtils');
goog.require('shaka.dash.SegmentBase');
goog.require('shaka.log');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.util.Error');
goog.require('shaka.util.Functional');
goog.require('shaka.util.XmlUtils');
/**
* @namespace shaka.dash.SegmentList
* @summary A set of functions for parsing SegmentList elements.
*/
/**
* Creates a new Stream object or updates the Stream in the manifest.
*
* @param {shaka.dash.DashParser.Context} context
* @param {!Object.<string, !shaka.media.SegmentIndex>} segmentIndexMap
* @return {shaka.dash.DashParser.StreamInfo}
*/
shaka.dash.SegmentList.createStream = function(context, segmentIndexMap) {
goog.asserts.assert(context.representation.segmentList,
'Should only be called with SegmentList');
var SegmentList = shaka.dash.SegmentList;
var init = shaka.dash.SegmentBase.createInitSegment(
context, SegmentList.fromInheritance_);
var info = SegmentList.parseSegmentListInfo_(context);
SegmentList.checkSegmentListInfo_(context, info);
/** @type {shaka.media.SegmentIndex} */
var segmentIndex = null;
var id = null;
if (context.period.id && context.representation.id) {
// Only check/store the index if period and representation IDs are set.
id = context.period.id + ',' + context.representation.id;
segmentIndex = segmentIndexMap[id];
}
var references = SegmentList.createSegmentReferences_(
context.periodInfo.duration, info.startNumber,
context.representation.baseUris, info);
shaka.dash.MpdUtils.fitSegmentReferences(
context.dynamic, context.periodInfo.duration, references);
if (segmentIndex) {
segmentIndex.merge(references);
var start = context.presentationTimeline.getSegmentAvailabilityStart();
segmentIndex.evict(start - context.periodInfo.start);
} else {
context.presentationTimeline.notifySegments(
context.periodInfo.start, references);
segmentIndex = new shaka.media.SegmentIndex(references);
if (id)
segmentIndexMap[id] = segmentIndex;
}
return {
createSegmentIndex: Promise.resolve.bind(Promise),
findSegmentPosition: segmentIndex.find.bind(segmentIndex),
getSegmentReference: segmentIndex.get.bind(segmentIndex),
initSegmentReference: init,
presentationTimeOffset: info.presentationTimeOffset
};
};
/**
* @typedef {{
* mediaUri: string,
* start: number,
* end: ?number
* }}
*
* @property {string} mediaUri
* The URI of the segment.
* @property {number} start
* The start byte of the segment.
* @property {?number} end
* The end byte of the segment, or null.
*/
shaka.dash.SegmentList.MediaSegment;
/**
* @typedef {{
* segmentDuration: ?number,
* startTime: number,
* startNumber: number,
* presentationTimeOffset: number,
* timeline: Array.<shaka.dash.MpdUtils.TimeRange>,
* mediaSegments: !Array.<shaka.dash.SegmentList.MediaSegment>
* }}
* @private
*
* @description
* Contains information about a SegmentList.
*
* @property {?number} segmentDuration
* The duration of the segments, if given.
* @property {number} startTime
* The start time of the first segment, in seconds.
* @property {number} startNumber
* The start number of the segments; 1 or greater.
* @property {number} presentationTimeOffset
* The presentationTimeOffset of the representation, in seconds.
* @property {Array.<shaka.dash.MpdUtils.TimeRange>} timeline
* The timeline of the representation, if given. Times in seconds.
* @property {!Array.<shaka.dash.SegmentList.MediaSegment>} mediaSegments
* The URI and byte-ranges of the media segments.
*/
shaka.dash.SegmentList.SegmentListInfo;
/**
* @param {?shaka.dash.DashParser.InheritanceFrame} frame
* @return {Element}
* @private
*/
shaka.dash.SegmentList.fromInheritance_ = function(frame) {
return frame.segmentList;
};
/**
* Parses the SegmentList items to create an info object.
*
* @param {shaka.dash.DashParser.Context} context
* @return {shaka.dash.SegmentList.SegmentListInfo}
* @private
*/
shaka.dash.SegmentList.parseSegmentListInfo_ = function(context) {
var SegmentList = shaka.dash.SegmentList;
var MpdUtils = shaka.dash.MpdUtils;
var mediaSegments = SegmentList.parseMediaSegments_(context);
var segmentInfo =
MpdUtils.parseSegmentInfo(context, SegmentList.fromInheritance_);
var startNumber = segmentInfo.startNumber;
if (startNumber === 0) {
shaka.log.warning('SegmentList@startNumber must be > 0');
startNumber = 1;
}
var startTime = 0;
if (segmentInfo.segmentDuration) {
// See DASH sec. 5.3.9.5.3
// Don't use presentationTimeOffset for @duration.
startTime = segmentInfo.segmentDuration * (startNumber - 1);
} else if (segmentInfo.timeline && segmentInfo.timeline.length > 0) {
// The presentationTimeOffset was considered in timeline creation
startTime = segmentInfo.timeline[0].start;
}
return {
segmentDuration: segmentInfo.segmentDuration,
startTime: startTime,
startNumber: startNumber,
presentationTimeOffset: segmentInfo.presentationTimeOffset,
timeline: segmentInfo.timeline,
mediaSegments: mediaSegments
};
};
/**
* Checks whether a SegmentListInfo object is valid.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.SegmentList.SegmentListInfo} info
* @throws shaka.util.Error When there is a parsing error.
* @private
*/
shaka.dash.SegmentList.checkSegmentListInfo_ = function(context, info) {
if (!info.segmentDuration && !info.timeline &&
info.mediaSegments.length > 1) {
shaka.log.warning(
'SegmentList does not contain sufficient segment information:',
'the SegmentList specifies multiple segments,',
'but does not specify a segment duration or timeline.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
}
if (!info.segmentDuration && !context.periodInfo.duration && !info.timeline &&
info.mediaSegments.length == 1) {
shaka.log.warning(
'SegmentList does not contain sufficient segment information:',
'the SegmentList specifies one segment,',
'but does not specify a segment duration, period duration,',
'or timeline.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
}
if (info.timeline && info.timeline.length == 0) {
shaka.log.warning(
'SegmentList does not contain sufficient segment information:',
'the SegmentList has an empty timeline.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
}
};
/**
* Creates an array of segment references for the given data.
*
* @param {?number} periodDuration in seconds.
* @param {number} startNumber
* @param {!Array.<string>} baseUris
* @param {shaka.dash.SegmentList.SegmentListInfo} info
* @return {!Array.<!shaka.media.SegmentReference>}
* @private
*/
shaka.dash.SegmentList.createSegmentReferences_ = function(
periodDuration, startNumber, baseUris, info) {
var MpdUtils = shaka.dash.MpdUtils;
var max = info.mediaSegments.length;
if (info.timeline && info.timeline.length != info.mediaSegments.length) {
max = Math.min(info.timeline.length, info.mediaSegments.length);
shaka.log.warning(
'The number of items in the segment timeline and the number of segment',
'URLs do not match, truncating', info.mediaSegments.length, 'to', max);
}
/** @type {!Array.<!shaka.media.SegmentReference>} */
var references = [];
var prevEndTime = info.startTime;
for (var i = 0; i < max; i++) {
var segment = info.mediaSegments[i];
var mediaUri = MpdUtils.resolveUris(baseUris, [segment.mediaUri]);
var startTime = prevEndTime;
var endTime;
if (info.segmentDuration != null) {
endTime = startTime + info.segmentDuration;
} else if (info.timeline) {
// Ignore the timepoint start since they are continuous.
endTime = info.timeline[i].end;
} else {
// If segmentDuration and timeline are null then there must
// only be one segment.
goog.asserts.assert(
info.mediaSegments.length == 1 && periodDuration,
'There should only be one segment with a Period duration.');
endTime = startTime + periodDuration;
}
var getUris = (function(uris) { return uris; }.bind(null, mediaUri));
references.push(
new shaka.media.SegmentReference(
i + startNumber, startTime, endTime, getUris, segment.start,
segment.end));
prevEndTime = endTime;
}
return references;
};
/**
* Parses the media URIs from the context.
*
* @param {shaka.dash.DashParser.Context} context
* @return {!Array.<shaka.dash.SegmentList.MediaSegment>}
* @private
*/
shaka.dash.SegmentList.parseMediaSegments_ = function(context) {
var Functional = shaka.util.Functional;
/** @type {!Array.<!Element>} */
var segmentLists = [
context.representation.segmentList,
context.adaptationSet.segmentList,
context.period.segmentList
].filter(Functional.isNotNull);
var XmlUtils = shaka.util.XmlUtils;
// Search each SegmentList for one with at least one SegmentURL element,
// select the first one, and convert each SegmentURL element to a tuple.
return segmentLists
.map(function(node) { return XmlUtils.findChildren(node, 'SegmentURL'); })
.reduce(function(all, part) { return all.length > 0 ? all : part; })
.map(function(urlNode) {
if (urlNode.getAttribute('indexRange') &&
!context.indexRangeWarningGiven) {
context.indexRangeWarningGiven = true;
shaka.log.warning(
'We do not support the SegmentURL@indexRange attribute on ' +
'SegmentList. We only use the SegmentList@duration attribute ' +
'or SegmentTimeline, which must be accurate.');
}
var uri = urlNode.getAttribute('media');
var range = XmlUtils.parseAttr(
urlNode, 'mediaRange', XmlUtils.parseRange, {start: 0, end: null});
return {mediaUri: uri, start: range.start, end: range.end};
});
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.dash.SegmentTemplate');
goog.require('goog.asserts');
goog.require('shaka.dash.MpdUtils');
goog.require('shaka.dash.SegmentBase');
goog.require('shaka.log');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.util.Error');
/**
* @namespace shaka.dash.SegmentTemplate
* @summary A set of functions for parsing SegmentTemplate elements.
*/
/**
* Creates a new Stream object or updates the Stream in the manifest.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.DashParser.RequestInitSegmentCallback} requestInitSegment
* @param {!Object.<string, !shaka.media.SegmentIndex>} segmentIndexMap
* @param {boolean} isUpdate True if the manifest is being updated.
* @throws shaka.util.Error When there is a parsing error.
* @return {shaka.dash.DashParser.StreamInfo}
*/
shaka.dash.SegmentTemplate.createStream = function(
context, requestInitSegment, segmentIndexMap, isUpdate) {
goog.asserts.assert(context.representation.segmentTemplate,
'Should only be called with SegmentTemplate');
var SegmentTemplate = shaka.dash.SegmentTemplate;
var init = SegmentTemplate.createInitSegment_(context);
var info = SegmentTemplate.parseSegmentTemplateInfo_(context);
SegmentTemplate.checkSegmentTemplateInfo_(context, info);
/** @type {?shaka.dash.DashParser.SegmentIndexFunctions} */
var segmentIndexFunctions = null;
if (info.indexTemplate) {
segmentIndexFunctions = SegmentTemplate.createFromIndexTemplate_(
context, requestInitSegment, init, info);
} else if (info.segmentDuration) {
if (!isUpdate) {
context.presentationTimeline.notifyMaxSegmentDuration(
info.segmentDuration);
}
segmentIndexFunctions = SegmentTemplate.createFromDuration_(context, info);
} else {
/** @type {shaka.media.SegmentIndex} */
var segmentIndex = null;
var id = null;
if (context.period.id && context.representation.id) {
// Only check/store the index if period and representation IDs are set.
id = context.period.id + ',' + context.representation.id;
segmentIndex = segmentIndexMap[id];
}
var references = SegmentTemplate.createFromTimeline_(context, info);
shaka.dash.MpdUtils.fitSegmentReferences(
context.dynamic, context.periodInfo.duration, references);
if (segmentIndex) {
segmentIndex.merge(references);
var start = context.presentationTimeline.getSegmentAvailabilityStart();
segmentIndex.evict(start - context.periodInfo.start);
} else {
context.presentationTimeline.notifySegments(
context.periodInfo.start, references);
segmentIndex = new shaka.media.SegmentIndex(references);
if (id)
segmentIndexMap[id] = segmentIndex;
}
segmentIndexFunctions = {
createSegmentIndex: Promise.resolve.bind(Promise),
findSegmentPosition: segmentIndex.find.bind(segmentIndex),
getSegmentReference: segmentIndex.get.bind(segmentIndex)
};
}
return {
createSegmentIndex: segmentIndexFunctions.createSegmentIndex,
findSegmentPosition: segmentIndexFunctions.findSegmentPosition,
getSegmentReference: segmentIndexFunctions.getSegmentReference,
initSegmentReference: init,
presentationTimeOffset: info.presentationTimeOffset
};
};
/**
* @typedef {{
* timescale: number,
* segmentDuration: ?number,
* startNumber: number,
* presentationTimeOffset: number,
* unscaledPresentationTimeOffset: number,
* timeline: Array.<shaka.dash.MpdUtils.TimeRange>,
* mediaTemplate: ?string,
* indexTemplate: ?string
* }}
* @private
*
* @description
* Contains information about a SegmentTemplate.
*
* @property {number} timescale
* The time-scale of the representation.
* @property {?number} segmentDuration
* The duration of the segments in seconds, if given.
* @property {number} startNumber
* The start number of the segments; 1 or greater.
* @property {number} presentationTimeOffset
* The presentationTimeOffset of the representation, in seconds.
* @property {number} unscaledPresentationTimeOffset
* The presentationTimeOffset of the representation, in timescale units.
* @property {Array.<shaka.dash.MpdUtils.TimeRange>} timeline
* The timeline of the representation, if given. Times in seconds.
* @property {?string} mediaTemplate
* The media URI template, if given.
* @property {?string} indexTemplate
* The index URI template, if given.
*/
shaka.dash.SegmentTemplate.SegmentTemplateInfo;
/**
* @param {?shaka.dash.DashParser.InheritanceFrame} frame
* @return {Element}
* @private
*/
shaka.dash.SegmentTemplate.fromInheritance_ = function(frame) {
return frame.segmentTemplate;
};
/**
* Parses a SegmentTemplate element into an info object.
*
* @param {shaka.dash.DashParser.Context} context
* @return {shaka.dash.SegmentTemplate.SegmentTemplateInfo}
* @private
*/
shaka.dash.SegmentTemplate.parseSegmentTemplateInfo_ = function(context) {
var SegmentTemplate = shaka.dash.SegmentTemplate;
var MpdUtils = shaka.dash.MpdUtils;
var segmentInfo =
MpdUtils.parseSegmentInfo(context, SegmentTemplate.fromInheritance_);
var media = MpdUtils.inheritAttribute(
context, SegmentTemplate.fromInheritance_, 'media');
var index = MpdUtils.inheritAttribute(
context, SegmentTemplate.fromInheritance_, 'index');
return {
segmentDuration: segmentInfo.segmentDuration,
timescale: segmentInfo.timescale,
startNumber: segmentInfo.startNumber,
presentationTimeOffset: segmentInfo.presentationTimeOffset,
unscaledPresentationTimeOffset: segmentInfo.unscaledPresentationTimeOffset,
timeline: segmentInfo.timeline,
mediaTemplate: media,
indexTemplate: index
};
};
/**
* Verifies a SegmentTemplate info object.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
* @throws shaka.util.Error When there is a parsing error.
* @private
*/
shaka.dash.SegmentTemplate.checkSegmentTemplateInfo_ = function(context, info) {
var n = 0;
n += info.indexTemplate ? 1 : 0;
n += info.timeline ? 1 : 0;
n += info.segmentDuration ? 1 : 0;
if (n == 0) {
shaka.log.error(
'SegmentTemplate does not contain any segment information:',
'the SegmentTemplate must contain either an index URL template',
'a SegmentTimeline, or a segment duration.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
} else if (n != 1) {
shaka.log.warning(
'SegmentTemplate containes multiple segment information sources:',
'the SegmentTemplate should only contain an index URL template,',
'a SegmentTimeline or a segment duration.',
context.representation);
if (info.indexTemplate) {
shaka.log.info('Using the index URL template by default.');
info.timeline = null;
info.segmentDuration = null;
} else {
goog.asserts.assert(info.timeline, 'There should be a timeline');
shaka.log.info('Using the SegmentTimeline by default.');
info.segmentDuration = null;
}
}
if (!info.indexTemplate && !info.mediaTemplate) {
shaka.log.error(
'SegmentTemplate does not contain sufficient segment information:',
'the SegmentTemplate\'s media URL template is missing.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
}
};
/**
* Creates segment index functions from a index URL template.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.DashParser.RequestInitSegmentCallback} requestInitSegment
* @param {shaka.media.InitSegmentReference} init
* @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
* @throws shaka.util.Error When there is a parsing error.
* @return {shaka.dash.DashParser.SegmentIndexFunctions}
* @private
*/
shaka.dash.SegmentTemplate.createFromIndexTemplate_ = function(
context, requestInitSegment, init, info) {
var MpdUtils = shaka.dash.MpdUtils;
// Determine the container type.
var containerType = context.representation.mimeType.split('/')[1];
if ((containerType != 'mp4') && (containerType != 'webm')) {
shaka.log.error(
'SegmentTemplate specifies an unsupported container type.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_UNSUPPORTED_CONTAINER);
}
if ((containerType == 'webm') && !init) {
shaka.log.error(
'SegmentTemplate does not contain sufficient segment information:',
'the SegmentTemplate uses a WebM container,',
'but does not contain an initialization URL template.',
context.representation);
throw new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.DASH_WEBM_MISSING_INIT);
}
goog.asserts.assert(info.indexTemplate, 'must be using index template');
var filledTemplate = MpdUtils.fillUriTemplate(
info.indexTemplate, context.representation.id,
null, context.bandwidth || null, null);
var resolvedUris =
MpdUtils.resolveUris(context.representation.baseUris, [filledTemplate]);
return shaka.dash.SegmentBase.createSegmentIndexFromUris(
context, requestInitSegment, init, resolvedUris, 0, null, containerType,
info.presentationTimeOffset);
};
/**
* Creates segment index functions from a segment duration.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
* @return {shaka.dash.DashParser.SegmentIndexFunctions}
* @private
*/
shaka.dash.SegmentTemplate.createFromDuration_ = function(context, info) {
goog.asserts.assert(info.mediaTemplate,
'There should be a media template with duration');
var MpdUtils = shaka.dash.MpdUtils;
var periodDuration = context.periodInfo.duration;
var segmentDuration = info.segmentDuration;
var startNumber = info.startNumber;
var timescale = info.timescale;
var template = info.mediaTemplate;
var bandwidth = context.bandwidth || null;
var id = context.representation.id;
var baseUris = context.representation.baseUris;
var find = function(periodTime) {
if (periodTime < 0)
return null;
else if (periodDuration && periodTime >= periodDuration)
return null;
return Math.floor(periodTime / segmentDuration);
};
var get = function(position) {
var segmentStart = position * segmentDuration;
// Do not construct segments references that should not exist.
if (segmentStart < 0)
return null;
else if (periodDuration && segmentStart >= periodDuration)
return null;
var getUris = function() {
var mediaUri = MpdUtils.fillUriTemplate(
template, id, position + startNumber, bandwidth,
segmentStart * timescale);
return MpdUtils.resolveUris(baseUris, [mediaUri]);
};
return new shaka.media.SegmentReference(
position, segmentStart, segmentStart + segmentDuration, getUris, 0,
null);
};
return {
createSegmentIndex: Promise.resolve.bind(Promise),
findSegmentPosition: find,
getSegmentReference: get
};
};
/**
* Creates segment references from a timeline.
*
* @param {shaka.dash.DashParser.Context} context
* @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
* @return {!Array.<!shaka.media.SegmentReference>}
* @private
*/
shaka.dash.SegmentTemplate.createFromTimeline_ = function(context, info) {
goog.asserts.assert(info.mediaTemplate,
'There should be a media template with a timeline');
var MpdUtils = shaka.dash.MpdUtils;
/** @type {!Array.<!shaka.media.SegmentReference>} */
var references = [];
for (var i = 0; i < info.timeline.length; i++) {
var start = info.timeline[i].start;
var unscaledStart = info.timeline[i].unscaledStart;
var end = info.timeline[i].end;
// Note: i = k - 1, where k indicates the k'th segment listed in the MPD.
// (See section 5.3.9.5.3 of the DASH spec.)
var segmentReplacement = i + info.startNumber;
// Consider the presentation time offset in segment uri computation
var timeReplacement = unscaledStart +
info.unscaledPresentationTimeOffset;
var createUris = (function(
template, repId, bandwidth, baseUris, segmentId, time) {
var mediaUri = MpdUtils.fillUriTemplate(
template, repId, segmentId, bandwidth, time);
return MpdUtils.resolveUris(baseUris, [mediaUri])
.map(function(g) { return g.toString(); });
}.bind(null, info.mediaTemplate, context.representation.id,
context.bandwidth || null, context.representation.baseUris,
segmentReplacement, timeReplacement));
references.push(new shaka.media.SegmentReference(
segmentReplacement, start, end, createUris, 0, null));
}
return references;
};
/**
* Creates an init segment reference from a context object.
*
* @param {shaka.dash.DashParser.Context} context
* @return {shaka.media.InitSegmentReference}
* @private
*/
shaka.dash.SegmentTemplate.createInitSegment_ = function(context) {
var MpdUtils = shaka.dash.MpdUtils;
var SegmentTemplate = shaka.dash.SegmentTemplate;
var initialization = MpdUtils.inheritAttribute(
context, SegmentTemplate.fromInheritance_, 'initialization');
if (!initialization)
return null;
var repId = context.representation.id;
var bandwidth = context.bandwidth || null;
var baseUris = context.representation.baseUris;
var getUris = function() {
goog.asserts.assert(initialization, 'Should have returned earler');
var filledTemplate = MpdUtils.fillUriTemplate(
initialization, repId, null, bandwidth, null);
var resolvedUris = MpdUtils.resolveUris(baseUris, [filledTemplate]);
return resolvedUris;
};
return new shaka.media.InitSegmentReference(getUris, 0, null);
};
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| asserts.js | 7.69% | (1 / 13) | 0% | (0 / 6) | 0% | (0 / 4) | 7.69% | (1 / 13) | |
| timer.js | 2.94% | (1 / 34) | 0% | (0 / 16) | 0% | (0 / 4) | 2.94% | (1 / 34) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('goog.asserts');
/**
* @namespace goog.asserts
* @summary An assertion framework which is compiled out for deployment.
* NOTE: this is not the closure library version. This uses the same name so
* the closure compiler will be able to use the conditions to assist type
* checking.
*/
/**
* @define {boolean} true to enable asserts, false otherwise.
*/
goog.define('goog.asserts.ENABLE_ASSERTS', goog.DEBUG);
/** @type {function(*, string)} */
goog.asserts.assert = function() {};
/** @private */
goog.asserts.patchAssert_ = function() {
var assert = console.assert;
if (!assert) {
console.assert = function() {};
} else if (!assert.bind) {
// IE 9 does not provide a .bind for the built-in console functions.
console.assert = function() {
assert.apply(console, arguments);
};
}
};
// Install assert functions.
if (goog.asserts.ENABLE_ASSERTS) {
goog.asserts.patchAssert_();
goog.asserts.assert = console.assert.bind(console);
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.timer');
goog.require('shaka.log');
/**
* @namespace shaka.timer
* @summary A performance timing framework.
* Used in both debugging and production builds.
*/
/**
* Begins a timer.
*
* @param {string} name
*/
shaka.timer.begin = function(name) {
shaka.timer.timers_[name] = {
begin: shaka.timer.now_(),
end: NaN
};
};
/**
* End a timer and log (debug) the elapsed time.
* Does nothing if the timer has not begun.
*
* @param {string} name
*/
shaka.timer.end = function(name) {
var record = shaka.timer.timers_[name];
if (!record) {
return;
}
record.end = shaka.timer.now_();
var diff = record.end - record.begin;
shaka.log.debug(name + ': ' + diff.toFixed(3) + 'ms');
};
/**
* Log (debug) the diff between two or more completed timers and return it.
* Does nothing if not all of the timers have begun.
*
* @param {string} name1
* @param {string} name2
* @param {...string} var_args
* @return {number} The diff between the timers, or NaN if they have not all
* completed.
*/
shaka.timer.diff = function(name1, name2, var_args) {
var t1 = shaka.timer.get(name1);
var t2 = shaka.timer.get(name2);
if (!t1 || !t2) {
return NaN;
}
var diff = t1 - t2;
var name = name1 + ' - ' + name2;
for (var i = 2; i < arguments.length; ++i) {
var name3 = arguments[i];
var t3 = shaka.timer.get(name3);
if (!t3) {
return NaN;
}
diff -= t3;
name += ' - ' + name3;
}
shaka.log.debug(name + ': ' + diff.toFixed(3) + 'ms');
return diff;
};
/**
* Query a timer.
*
* @param {string} name
* @return {number} The elapsed time in milliseconds, if the timer is complete.
* Returns NaN if the timer doesn't exist or hasn't ended yet.
*/
shaka.timer.get = function(name) {
var record = shaka.timer.timers_[name];
if (!record || !record.end) {
return NaN;
}
return record.end - record.begin;
};
/** @private {function():number} */
shaka.timer.now_ = window.performance && window.performance.now ?
window.performance.now.bind(window.performance) :
Date.now;
/** @private {!Object.<string, {begin: number, end: number}>} */
shaka.timer.timers_ = {};
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| drm_engine.js | 0.43% | (2 / 462) | 0% | (0 / 208) | 0% | (0 / 71) | 0.45% | (2 / 444) | |
| manifest_parser.js | 1.96% | (1 / 51) | 0% | (0 / 10) | 0% | (0 / 8) | 1.96% | (1 / 51) | |
| media_source_engine.js | 0.45% | (1 / 223) | 0% | (0 / 66) | 0% | (0 / 37) | 0.46% | (1 / 219) | |
| mp4_segment_index_parser.js | 2.08% | (1 / 48) | 0% | (0 / 8) | 0% | (0 / 2) | 2.08% | (1 / 48) | |
| mp4_ttml_parser.js | 5.88% | (1 / 17) | 0% | (0 / 4) | 0% | (0 / 1) | 5.88% | (1 / 17) | |
| mp4_vtt_parser.js | 1.28% | (1 / 78) | 0% | (0 / 26) | 0% | (0 / 3) | 1.28% | (1 / 78) | |
| playhead.js | 0.57% | (1 / 175) | 0% | (0 / 84) | 0% | (0 / 21) | 0.58% | (1 / 171) | |
| presentation_timeline.js | 1.45% | (1 / 69) | 0% | (0 / 29) | 0% | (0 / 18) | 1.45% | (1 / 69) | |
| segment_index.js | 1.28% | (1 / 78) | 0% | (0 / 43) | 0% | (0 / 8) | 1.32% | (1 / 76) | |
| segment_reference.js | 6.25% | (1 / 16) | 0% | (0 / 2) | 0% | (0 / 2) | 6.25% | (1 / 16) | |
| streaming_engine.js | 0.16% | (1 / 610) | 0% | (0 / 246) | 0% | (0 / 68) | 0.17% | (1 / 584) | |
| text_engine.js | 1.1% | (1 / 91) | 0% | (0 / 46) | 0% | (0 / 18) | 1.18% | (1 / 85) | |
| time_ranges_utils.js | 2.04% | (1 / 49) | 0% | (0 / 47) | 0% | (0 / 4) | 2.44% | (1 / 41) | |
| ttml_text_parser.js | 0.45% | (1 / 221) | 0% | (0 / 124) | 0% | (0 / 14) | 0.45% | (1 / 221) | |
| vtt_text_parser.js | 1.04% | (1 / 96) | 0% | (0 / 59) | 0% | (0 / 4) | 1.04% | (1 / 96) | |
| webm_segment_index_parser.js | 0.89% | (1 / 112) | 0% | (0 / 30) | 0% | (0 / 8) | 0.9% | (1 / 111) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 | 2 1 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.DrmEngine');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.ArrayUtils');
goog.require('shaka.util.Error');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.Functional');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.MapUtils');
goog.require('shaka.util.PublicPromise');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.Timer');
goog.require('shaka.util.Uint8ArrayUtils');
/**
* @constructor
* @param {!shaka.net.NetworkingEngine} networkingEngine
* @param {function(!shaka.util.Error)} onError Called when an error occurs.
* @param {function(!Object.<string, string>)} onKeyStatus Called when key
* status changes. Argument is a map of hex key IDs to statuses.
* @struct
* @implements {shaka.util.IDestroyable}
*/
shaka.media.DrmEngine = function(networkingEngine, onError, onKeyStatus) {
/** @private {Array.<string>} */
this.supportedTypes_ = null;
/** @private {MediaKeys} */
this.mediaKeys_ = null;
/** @private {HTMLMediaElement} */
this.video_ = null;
/** @private {boolean} */
this.initialized_ = false;
/** @private {?shakaExtern.DrmInfo} */
this.currentDrmInfo_ = null;
/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/** @private {!Array.<shaka.media.DrmEngine.ActiveSession>} */
this.activeSessions_ = [];
/** @private {!Array.<string>} */
this.offlineSessionIds_ = [];
/** @private {!shaka.util.PublicPromise} */
this.allSessionsLoaded_ = new shaka.util.PublicPromise();
/** @private {shaka.net.NetworkingEngine} */
this.networkingEngine_ = networkingEngine;
/** @private {?shakaExtern.DrmConfiguration} */
this.config_ = null;
/** @private {?function(!shaka.util.Error)} */
this.onError_ = (function(err) {
this.allSessionsLoaded_.reject(err);
onError(err);
}.bind(this));
/** @private {!Object.<string, string>} */
this.keyStatusByKeyId_ = {};
/** @private {?function(!Object.<string, string>)} */
this.onKeyStatus_ = onKeyStatus;
/** @private {shaka.util.Timer} */
this.keyStatusTimer_ = new shaka.util.Timer(
this.processKeyStatusChanges_.bind(this));
/** @private {boolean} */
this.destroyed_ = false;
/** @private {boolean} */
this.isOffline_ = false;
// Add a catch to the Promise to avoid console logs about uncaught errors.
this.allSessionsLoaded_.catch(function() {});
};
/**
* @typedef {{
* loaded: boolean,
* initData: Uint8Array,
* session: !MediaKeySession,
* updatePromise: shaka.util.PublicPromise
* }}
*
* @description A record to track sessions and suppress duplicate init data.
* @property {boolean} loaded
* True once the key status has been updated (to a non-pending state). This
* does not mean the session is 'usable'.
* @property {Uint8Array} initData
* The init data used to create the session.
* @property {!MediaKeySession} session
* The session object.
* @property {shaka.util.PublicPromise} updatePromise
* An optional Promise that will be resolved/rejected on the next update()
* call. This is used to track the 'license-release' message when calling
* remove().
*/
shaka.media.DrmEngine.ActiveSession;
/** @override */
shaka.media.DrmEngine.prototype.destroy = function() {
var Functional = shaka.util.Functional;
this.destroyed_ = true;
var async = this.activeSessions_.map(function(activeSession) {
// Ignore any errors when closing the sessions. One such error would be
// an invalid state error triggered by closing a session which has not
// generated any key requests.
// Chrome sometimes returns |undefined|: https://crbug.com/690664
var p = activeSession.session.close() || Promise.resolve();
return p.catch(Functional.noop);
});
this.allSessionsLoaded_.reject();
if (this.eventManager_)
async.push(this.eventManager_.destroy());
if (this.video_) {
goog.asserts.assert(!this.video_.src, 'video src must be removed first!');
async.push(this.video_.setMediaKeys(null).catch(Functional.noop));
}
if (this.keyStatusTimer_) {
this.keyStatusTimer_.cancel();
}
this.keyStatusTimer_ = null;
this.currentDrmInfo_ = null;
this.supportedTypes_ = null;
this.mediaKeys_ = null;
this.video_ = null;
this.eventManager_ = null;
this.activeSessions_ = [];
this.offlineSessionIds_ = [];
this.networkingEngine_ = null; // We don't own it, don't destroy() it.
this.config_ = null;
this.onError_ = null;
return Promise.all(async);
};
/**
* Called by the Player to provide an updated configuration any time it changes.
* Must be called at least once before init().
*
* @param {shakaExtern.DrmConfiguration} config
*/
shaka.media.DrmEngine.prototype.configure = function(config) {
this.config_ = config;
};
/**
* Negotiate for a key system and set up MediaKeys.
* @param {!shakaExtern.Manifest} manifest The manifest is read for MIME type
* and DRM information to query EME. If the 'clearKeys' configuration is
* used, the manifest will be modified to force the use of Clear Key.
* @param {boolean} offline True if we are storing or loading offline content.
* @return {!Promise} Resolved if/when a key system has been chosen.
*/
shaka.media.DrmEngine.prototype.init = function(manifest, offline) {
goog.asserts.assert(this.config_,
'DrmEngine configure() must be called before init()!');
/** @type {!Object.<string, MediaKeySystemConfiguration>} */
var configsByKeySystem = {};
/** @type {!Array.<string>} */
var keySystemsInOrder = [];
// |isOffline_| determines what kind of session to create. The argument to
// |prepareMediaKeyConfigs_| determines the kind of CDM to query for. So
// we still need persistent state when we are loading offline sessions.
this.isOffline_ = offline;
this.offlineSessionIds_ = manifest.offlineSessionIds;
this.prepareMediaKeyConfigs_(
manifest, offline || manifest.offlineSessionIds.length > 0,
configsByKeySystem, keySystemsInOrder);
if (!keySystemsInOrder.length) {
// Unencrypted.
this.initialized_ = true;
return Promise.resolve();
}
return this.queryMediaKeys_(configsByKeySystem, keySystemsInOrder);
};
/**
* Attach MediaKeys to the video element and start processing events.
* @param {HTMLMediaElement} video
* @return {!Promise}
*/
shaka.media.DrmEngine.prototype.attach = function(video) {
if (!this.mediaKeys_) {
// Unencrypted, or so we think. We listen for encrypted events in order to
// warn when the stream is encrypted, even though the manifest does not know
// it.
this.eventManager_.listen(video, 'encrypted', function(event) {
// Don't complain about this twice.
this.eventManager_.unlisten(video, 'encrypted');
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.ENCRYPTED_CONTENT_WITHOUT_DRM_INFO));
}.bind(this));
return Promise.resolve();
}
this.video_ = video;
var setMediaKeys = this.video_.setMediaKeys(this.mediaKeys_);
setMediaKeys = setMediaKeys.catch(function(exception) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.FAILED_TO_ATTACH_TO_VIDEO,
exception.message));
});
var setServerCertificate = null;
if (this.currentDrmInfo_.serverCertificate) {
setServerCertificate = this.mediaKeys_.setServerCertificate(
this.currentDrmInfo_.serverCertificate);
setServerCertificate = setServerCertificate.catch(function(exception) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.INVALID_SERVER_CERTIFICATE,
exception.message));
});
}
return Promise.all([setMediaKeys, setServerCertificate]).then(function() {
if (this.destroyed_) return Promise.reject();
this.createOrLoad();
if (!this.currentDrmInfo_.initData.length &&
!this.offlineSessionIds_.length) {
// Explicit init data for any one stream or an offline session is
// sufficient to suppress 'encrypted' events for all streams.
var onEncrypted = /** @type {shaka.util.EventManager.ListenerType} */(
this.onEncrypted_.bind(this));
this.eventManager_.listen(this.video_, 'encrypted', onEncrypted);
}
}.bind(this)).catch(function(error) {
if (this.destroyed_) return Promise.resolve(); // Ignore destruction errors
return Promise.reject(error);
}.bind(this));
};
/**
* Removes the given offline sessions and deletes their data. Must call init()
* before this. This will wait until the 'license-release' message is handled
* and the resulting Promise will be rejected if there is an error with that.
*
* @param {!Array.<string>} sessions
* @return {!Promise}
*/
shaka.media.DrmEngine.prototype.removeSessions = function(sessions) {
goog.asserts.assert(this.mediaKeys_ || !sessions.length,
'Must call init() before removeSessions');
return Promise.all(sessions.map(function(sessionId) {
return this.loadOfflineSession_(sessionId).then(function(session) {
// This will be null on error, such as session not found.
if (session) {
var p = new shaka.util.PublicPromise();
// TODO: Consider adding a timeout to get the 'message' event.
// Note that the 'message' event will get raised after the remove()
// promise resolves.
for (var i = 0; i < this.activeSessions_.length; i++) {
if (this.activeSessions_[i].session == session) {
this.activeSessions_[i].updatePromise = p;
break;
}
}
return Promise.all([session.remove(), p]);
}
}.bind(this));
}.bind(this)));
};
/**
* Creates the sessions for the init data and waits for them to become ready.
*
* @return {!Promise}
*/
shaka.media.DrmEngine.prototype.createOrLoad = function() {
var initDatas = this.currentDrmInfo_ ? this.currentDrmInfo_.initData : [];
initDatas.forEach(function(initDataOverride) {
this.createTemporarySession_(
initDataOverride.initDataType, initDataOverride.initData);
}.bind(this));
this.offlineSessionIds_.forEach(function(sessionId) {
this.loadOfflineSession_(sessionId);
}.bind(this));
if (!initDatas.length && !this.offlineSessionIds_.length)
this.allSessionsLoaded_.resolve();
return this.allSessionsLoaded_;
};
/** @return {boolean} */
shaka.media.DrmEngine.prototype.initialized = function() {
return this.initialized_;
};
/** @return {string} */
shaka.media.DrmEngine.prototype.keySystem = function() {
return this.currentDrmInfo_ ? this.currentDrmInfo_.keySystem : '';
};
/**
* Returns an array of the media types supported by the current key system.
* These will be full mime types (e.g. 'video/webm; codecs="vp8"').
*
* @return {Array.<string>}
*/
shaka.media.DrmEngine.prototype.getSupportedTypes = function() {
return this.supportedTypes_;
};
/**
* Returns the ID of the sessions currently active.
*
* @return {!Array.<string>}
*/
shaka.media.DrmEngine.prototype.getSessionIds = function() {
return this.activeSessions_.map(function(session) {
return session.session.sessionId;
});
};
/**
* Returns the DrmInfo that was used to initialize the current key system.
*
* @return {?shakaExtern.DrmInfo}
*/
shaka.media.DrmEngine.prototype.getDrmInfo = function() {
return this.currentDrmInfo_;
};
/**
* @param {!shakaExtern.Manifest} manifest
* @param {boolean} offline True if we are storing or loading offline content.
* @param {!Object.<string, MediaKeySystemConfiguration>} configsByKeySystem
* (Output parameter.) A dictionary of configs, indexed by key system.
* @param {!Array.<string>} keySystemsInOrder
* (Output parameter.) A list of key systems in the order in which we
* encounter them.
* @see https://goo.gl/nwdYnY for MediaKeySystemConfiguration spec
* @private
*/
shaka.media.DrmEngine.prototype.prepareMediaKeyConfigs_ =
function(manifest, offline, configsByKeySystem, keySystemsInOrder) {
var clearKeyDrmInfo = this.configureClearKey_();
manifest.periods.forEach(function(period) {
period.streamSets.forEach(function(streamSet) {
if (streamSet.type == 'text')
return; // skip
// clearKey config overrides manifest DrmInfo if present.
// The manifest is modified so that filtering in Player still works.
if (clearKeyDrmInfo) {
streamSet.drmInfos = [clearKeyDrmInfo];
}
streamSet.drmInfos.forEach(function(drmInfo) {
this.fillInDrmInfoDefaults_(drmInfo);
var config = configsByKeySystem[drmInfo.keySystem];
if (!config) {
config = {
// ignore initDataTypes
audioCapabilities: [],
videoCapabilities: [],
distinctiveIdentifier: 'optional',
persistentState: offline ? 'required' : 'optional',
sessionTypes: [offline ? 'persistent-license' : 'temporary'],
label: drmInfo.keySystem,
drmInfos: [] // tracked by us, ignored by EME
};
configsByKeySystem[drmInfo.keySystem] = config;
keySystemsInOrder.push(drmInfo.keySystem);
}
config.drmInfos.push(drmInfo);
if (drmInfo.distinctiveIdentifierRequired)
config.distinctiveIdentifier = 'required';
if (drmInfo.persistentStateRequired)
config.persistentState = 'required';
/** @type {!Array.<!MediaKeySystemMediaCapability>} */
var capabilities = (streamSet.type == 'video') ?
config.videoCapabilities : config.audioCapabilities;
/** @type {string} */
var robustness = ((streamSet.type == 'video') ?
drmInfo.videoRobustness : drmInfo.audioRobustness) || '';
streamSet.streams.forEach(function(stream) {
var fullMimeType = stream.mimeType;
if (stream.codecs) {
fullMimeType += '; codecs="' + stream.codecs + '"';
}
// Some DRM license providers requires that we have a default
// KID from manifest in the wrapped license request
// and needs to be accessible in a request filter
if (stream.keyId) {
drmInfo.keyIds.push(stream.keyId);
}
capabilities.push({
robustness: robustness,
contentType: fullMimeType
});
}.bind(this)); // streamSet.streams.forEach
}.bind(this)); // streamSet.drmInfos.forEach
}.bind(this)); // period.streamSets.forEach
}.bind(this)); // manifest.perios.forEach
};
/**
* @param {!Object.<string, MediaKeySystemConfiguration>} configsByKeySystem
* A dictionary of configs, indexed by key system.
* @param {!Array.<string>} keySystemsInOrder
* A list of key systems in the order in which we should query them.
* On a browser which supports multiple key systems, the order may indicate
* a real preference for the application.
* @return {!Promise} Resolved if/when a key system has been chosen.
* @private
*/
shaka.media.DrmEngine.prototype.queryMediaKeys_ =
function(configsByKeySystem, keySystemsInOrder) {
if (keySystemsInOrder.length == 1 && keySystemsInOrder[0] == '') {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.NO_RECOGNIZED_KEY_SYSTEMS));
}
// Wait to reject this initial Promise until we have built the entire chain.
var instigator = new shaka.util.PublicPromise();
var p = instigator;
// Try key systems with configured license servers first. We only have to try
// key systems without configured license servers for diagnostic reasons, so
// that we can differentiate between "none of these key systems are available"
// and "some are available, but you did not configure them properly." The
// former takes precedence.
[true, false].forEach(function(shouldHaveLicenseServer) {
keySystemsInOrder.forEach(function(keySystem) {
var config = configsByKeySystem[keySystem];
var hasLicenseServer = config.drmInfos.some(function(info) {
return !!info.licenseServerUri;
});
if (hasLicenseServer != shouldHaveLicenseServer) return;
// If there are no tracks of a type, these should be not present.
// Otherwise the query will fail.
if (config.audioCapabilities.length == 0) {
delete config.audioCapabilities;
}
if (config.videoCapabilities.length == 0) {
delete config.videoCapabilities;
}
p = p.catch(function() {
if (this.destroyed_) return Promise.reject();
return navigator.requestMediaKeySystemAccess(keySystem, [config]);
}.bind(this));
}.bind(this));
}.bind(this));
p = p.catch(function() {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE));
});
p = p.then(function(mediaKeySystemAccess) {
if (this.destroyed_) return Promise.reject();
// TODO: Remove once Edge has released a fix for https://goo.gl/qMeV7v
var isEdge = navigator.userAgent.indexOf('Edge/') >= 0;
// Store the capabilities of the key system.
var realConfig = mediaKeySystemAccess.getConfiguration();
var audioCaps = realConfig.audioCapabilities || [];
var videoCaps = realConfig.videoCapabilities || [];
var caps = audioCaps.concat(videoCaps);
this.supportedTypes_ = caps.map(function(c) { return c.contentType; });
if (isEdge) {
// Edge 14 does not report correct capabilities. It will only report the
// first MIME type even if the others are supported. To work around this,
// set the supported types to null, which Player will use as a signal that
// the information is not available.
// See: https://goo.gl/qMeV7v
this.supportedTypes_ = null;
}
goog.asserts.assert(!this.supportedTypes_ || this.supportedTypes_.length,
'We should get at least one supported MIME type');
var originalConfig = configsByKeySystem[mediaKeySystemAccess.keySystem];
this.createCurrentDrmInfo_(
mediaKeySystemAccess.keySystem, originalConfig,
originalConfig.drmInfos);
if (!this.currentDrmInfo_.licenseServerUri) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.NO_LICENSE_SERVER_GIVEN));
}
return mediaKeySystemAccess.createMediaKeys();
}.bind(this)).then(function(mediaKeys) {
if (this.destroyed_) return Promise.reject();
this.mediaKeys_ = mediaKeys;
this.initialized_ = true;
}.bind(this)).catch(function(exception) {
if (this.destroyed_) return Promise.resolve(); // Ignore destruction errors
// Don't rewrap a shaka.util.Error from earlier in the chain:
this.currentDrmInfo_ = null;
this.supportedTypes_ = null;
if (exception instanceof shaka.util.Error) {
return Promise.reject(exception);
}
// We failed to create MediaKeys. This generally shouldn't happen.
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.FAILED_TO_CREATE_CDM,
exception.message));
}.bind(this));
instigator.reject();
return p;
};
/**
* Use this.config_ to fill in missing values in drmInfo.
* @param {shakaExtern.DrmInfo} drmInfo
* @private
*/
shaka.media.DrmEngine.prototype.fillInDrmInfoDefaults_ = function(drmInfo) {
var keySystem = drmInfo.keySystem;
if (!keySystem) {
// This is a placeholder from the manifest parser for an unrecognized key
// system. Skip this entry, to avoid logging nonsensical errors.
return;
}
if (!drmInfo.licenseServerUri) {
var server = this.config_.servers[keySystem];
if (server) {
drmInfo.licenseServerUri = server;
} else {
shaka.log.error('No license server configured for ' + keySystem);
}
}
if (!drmInfo.keyIds) {
drmInfo.keyIds = [];
}
var advanced = this.config_.advanced[keySystem];
if (advanced) {
if (!drmInfo.distinctiveIdentifierRequired) {
drmInfo.distinctiveIdentifierRequired =
advanced.distinctiveIdentifierRequired;
}
if (!drmInfo.persistentStateRequired) {
drmInfo.persistentStateRequired = advanced.persistentStateRequired;
}
if (!drmInfo.videoRobustness) {
drmInfo.videoRobustness = advanced.videoRobustness;
}
if (!drmInfo.audioRobustness) {
drmInfo.audioRobustness = advanced.audioRobustness;
}
if (!drmInfo.serverCertificate) {
drmInfo.serverCertificate = advanced.serverCertificate;
}
}
};
/**
* Create a DrmInfo using configured clear keys.
* The server URI will be a data URI which decodes to a clearkey license.
* @return {?shakaExtern.DrmInfo} or null if clear keys are not configured.
* @private
* @see https://goo.gl/6nPdhF for the spec on the clearkey license format.
*/
shaka.media.DrmEngine.prototype.configureClearKey_ = function() {
var hasClearKeys = !shaka.util.MapUtils.empty(this.config_.clearKeys);
if (!hasClearKeys) return null;
var StringUtils = shaka.util.StringUtils;
var Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
var keys = [];
var keyIds = [];
for (var keyIdHex in this.config_.clearKeys) {
var keyHex = this.config_.clearKeys[keyIdHex];
var keyId = Uint8ArrayUtils.fromHex(keyIdHex);
var key = Uint8ArrayUtils.fromHex(keyHex);
var keyObj = {
kty: 'oct',
kid: Uint8ArrayUtils.toBase64(keyId, false),
k: Uint8ArrayUtils.toBase64(key, false)
};
keys.push(keyObj);
keyIds.push(keyObj.kid);
}
var jwkSet = {keys: keys};
var license = JSON.stringify(jwkSet);
// Use the keyids init data since is suggested by EME.
// Suggestion: https://goo.gl/R72xp4
// Format: https://goo.gl/75RCP6
var initDataStr = JSON.stringify({'kids': keyIds});
var initData = new Uint8Array(StringUtils.toUTF8(initDataStr));
var initDatas = [{initData: initData, initDataType: 'keyids'}];
return {
keySystem: 'org.w3.clearkey',
licenseServerUri: 'data:application/json;base64,' + window.btoa(license),
distinctiveIdentifierRequired: false,
persistentStateRequired: false,
audioRobustness: '',
videoRobustness: '',
serverCertificate: null,
initData: initDatas,
keyIds: []
};
};
/**
* Creates a DrmInfo object describing the settings used to initialize the
* engine.
*
* @param {string} keySystem
* @param {MediaKeySystemConfiguration} config
* @param {!Array.<shakaExtern.DrmInfo>} drmInfos
* @private
*/
shaka.media.DrmEngine.prototype.createCurrentDrmInfo_ = function(
keySystem, config, drmInfos) {
/** @type {!Array.<string>} */
var licenseServers = [];
/** @type {!Array.<!Uint8Array>} */
var serverCerts = [];
/** @type {!Array.<!shakaExtern.InitDataOverride>} */
var initDatas = [];
/** @type {!Array.<string>} */
var keyIds = [];
this.processDrmInfos_(drmInfos, licenseServers, serverCerts, initDatas,
keyIds);
if (serverCerts.length > 1) {
shaka.log.warning('Multiple unique server certificates found! ' +
'Only the first will be used.');
}
if (licenseServers.length > 1) {
shaka.log.warning('Multiple unique license server URIs found! ' +
'Only the first will be used.');
}
// TODO: This only works when all DrmInfo have the same robustness.
var audioRobustness =
config.audioCapabilities ? config.audioCapabilities[0].robustness : '';
var videoRobustness =
config.videoCapabilities ? config.videoCapabilities[0].robustness : '';
this.currentDrmInfo_ = {
keySystem: keySystem,
licenseServerUri: licenseServers[0],
distinctiveIdentifierRequired: (config.distinctiveIdentifier == 'required'),
persistentStateRequired: (config.persistentState == 'required'),
audioRobustness: audioRobustness,
videoRobustness: videoRobustness,
serverCertificate: serverCerts[0],
initData: initDatas,
keyIds: keyIds
};
};
/**
* Extract license server, server cert, and init data from DrmInfos, taking
* care to eliminate duplicates.
*
* @param {!Array.<shakaExtern.DrmInfo>} drmInfos
* @param {!Array.<string>} licenseServers
* @param {!Array.<!Uint8Array>} serverCerts
* @param {!Array.<!shakaExtern.InitDataOverride>} initDatas
* @param {!Array.<string>} keyIds
* @private
*/
shaka.media.DrmEngine.prototype.processDrmInfos_ =
function(drmInfos, licenseServers, serverCerts, initDatas, keyIds) {
/**
* @param {shakaExtern.InitDataOverride} a
* @param {shakaExtern.InitDataOverride} b
* @return {boolean}
*/
function initDataOverrideEqual(a, b) {
return a.initDataType == b.initDataType &&
shaka.util.Uint8ArrayUtils.equal(a.initData, b.initData);
}
drmInfos.forEach(function(drmInfo) {
// Aliases:
var ArrayUtils = shaka.util.ArrayUtils;
var Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
// Build an array of unique license servers.
if (licenseServers.indexOf(drmInfo.licenseServerUri) == -1) {
licenseServers.push(drmInfo.licenseServerUri);
}
// Build an array of unique server certs.
if (drmInfo.serverCertificate) {
if (ArrayUtils.indexOf(serverCerts, drmInfo.serverCertificate,
Uint8ArrayUtils.equal) == -1) {
serverCerts.push(drmInfo.serverCertificate);
}
}
// Build an array of unique init datas.
if (drmInfo.initData) {
drmInfo.initData.forEach(function(initDataOverride) {
if (ArrayUtils.indexOf(initDatas, initDataOverride,
initDataOverrideEqual) == -1) {
initDatas.push(initDataOverride);
}
});
}
if (drmInfo.keyIds) {
for (var i = 0; i < drmInfo.keyIds.length; ++i) {
if (keyIds.indexOf(drmInfo.keyIds[i]) == -1) {
keyIds.push(drmInfo.keyIds[i]);
}
}
}
});
};
/**
* @param {!MediaEncryptedEvent} event
* @private
*/
shaka.media.DrmEngine.prototype.onEncrypted_ = function(event) {
// Aliases:
var Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
var initData = new Uint8Array(event.initData);
// Suppress duplicate init data.
// Note that some init data are extremely large and can't portably be used as
// keys in a dictionary.
for (var i = 0; i < this.activeSessions_.length; ++i) {
if (Uint8ArrayUtils.equal(initData, this.activeSessions_[i].initData)) {
shaka.log.debug('Ignoring duplicate init data.');
return;
}
}
this.createTemporarySession_(event.initDataType, initData);
};
/**
* @param {string} sessionId
* @return {!Promise.<MediaKeySession>}
* @private
*/
shaka.media.DrmEngine.prototype.loadOfflineSession_ = function(sessionId) {
var session;
try {
session = this.mediaKeys_.createSession('persistent-license');
} catch (exception) {
var error = new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.FAILED_TO_CREATE_SESSION,
exception.message);
this.onError_(error);
return Promise.reject(error);
}
this.eventManager_.listen(session, 'message',
/** @type {shaka.util.EventManager.ListenerType} */(
this.onSessionMessage_.bind(this)));
this.eventManager_.listen(session, 'keystatuseschange',
this.onKeyStatusesChange_.bind(this));
var activeSession =
{initData: null, session: session, loaded: false, updatePromise: null};
this.activeSessions_.push(activeSession);
return session.load(sessionId).then(function(present) {
if (this.destroyed_) return;
if (!present) {
var i = this.activeSessions_.indexOf(activeSession);
goog.asserts.assert(i >= 0, 'Session must be in the array');
this.activeSessions_.splice(i, 1);
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.OFFLINE_SESSION_REMOVED));
return;
}
// TODO: We should get a key status change event. Remove once Chrome CDM
// is fixed.
activeSession.loaded = true;
if (this.activeSessions_.every(function(s) { return s.loaded; }))
this.allSessionsLoaded_.resolve();
return session;
}.bind(this), function(error) {
if (this.destroyed_) return;
var i = this.activeSessions_.indexOf(activeSession);
goog.asserts.assert(i >= 0, 'Session must be in the array');
this.activeSessions_.splice(i, 1);
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.FAILED_TO_CREATE_SESSION,
error.message));
}.bind(this));
};
/**
* @param {string} initDataType
* @param {!Uint8Array} initData
* @private
*/
shaka.media.DrmEngine.prototype.createTemporarySession_ =
function(initDataType, initData) {
var session;
try {
if (this.isOffline_) {
session = this.mediaKeys_.createSession('persistent-license');
} else {
session = this.mediaKeys_.createSession();
}
} catch (exception) {
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.FAILED_TO_CREATE_SESSION,
exception.message));
return;
}
this.eventManager_.listen(session, 'message',
/** @type {shaka.util.EventManager.ListenerType} */(
this.onSessionMessage_.bind(this)));
this.eventManager_.listen(session, 'keystatuseschange',
this.onKeyStatusesChange_.bind(this));
this.activeSessions_.push({
initData: initData,
session: session,
loaded: false,
updatePromise: null
});
session.generateRequest(initDataType, initData.buffer).catch(function(error) {
if (this.destroyed_) return;
for (var i = 0; i < this.activeSessions_.length; ++i) {
if (this.activeSessions_[i].session == session) {
this.activeSessions_.splice(i, 1);
break;
}
}
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.FAILED_TO_GENERATE_LICENSE_REQUEST,
error.message));
}.bind(this));
};
/**
* @param {!MediaKeyMessageEvent} event
* @private
*/
shaka.media.DrmEngine.prototype.onSessionMessage_ = function(event) {
/** @type {!MediaKeySession} */
var session = event.target;
var updatePromise;
for (var i = 0; i < this.activeSessions_.length; i++) {
if (this.activeSessions_[i].session == session) {
updatePromise = this.activeSessions_[i].updatePromise;
break;
}
}
var requestType = shaka.net.NetworkingEngine.RequestType.LICENSE;
var request = shaka.net.NetworkingEngine.makeRequest(
[this.currentDrmInfo_.licenseServerUri], this.config_.retryParameters);
request.body = event.message;
request.method = 'POST';
// NOTE: allowCrossSiteCredentials can be set in a request filter.
if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready') {
this.unpackPlayReadyRequest_(request);
}
this.networkingEngine_.request(requestType, request)
.then(function(response) {
if (this.destroyed_) return Promise.reject();
// Request succeeded, now pass the response to the CDM.
return session.update(response.data).then(function() {
if (updatePromise)
updatePromise.resolve();
});
}.bind(this), function(error) {
// Ignore destruction errors
if (this.destroyed_) return Promise.resolve();
// Request failed!
goog.asserts.assert(error instanceof shaka.util.Error,
'Wrong NetworkingEngine error type!');
var shakaErr = new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.LICENSE_REQUEST_FAILED,
error);
this.onError_(shakaErr);
if (updatePromise)
updatePromise.reject(shakaErr);
}.bind(this)).catch(function(error) {
// Ignore destruction errors
if (this.destroyed_) return Promise.resolve();
// Session update failed!
var shakaErr = new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.LICENSE_RESPONSE_REJECTED,
error.message);
this.onError_(shakaErr);
if (updatePromise)
updatePromise.reject(shakaErr);
}.bind(this));
};
/**
* Unpack PlayReady license requests. Modifies the request object.
* @param {shakaExtern.Request} request
* @private
*/
shaka.media.DrmEngine.prototype.unpackPlayReadyRequest_ = function(request) {
// The PlayReady license message as it comes from the CDM can't be directly
// delivered to a license server. Other CDMs do not seem to need this kind
// of special handling.
// The raw license message is UTF-16-encoded XML. We need to unpack the
// Challenge element (base64-encoded string containing the actual license
// request) and any HttpHeader elements (sent as request headers).
// Example XML:
// <PlayReadyKeyMessage type="LicenseAcquisition">
// <LicenseAcquisition Version="1">
// <Challenge encoding="base64encoded">{Base64Data}</Challenge>
// <HttpHeaders>
// <HttpHeader>
// <name>Content-Type</name>
// <value>text/xml; charset=utf-8</value>
// </HttpHeader>
// <HttpHeader>
// <name>SOAPAction</name>
// <value>http://schemas.microsoft.com/DRM/etc/etc</value>
// </HttpHeader>
// </HttpHeaders>
// </LicenseAcquisition>
// </PlayReadyKeyMessage>
var xml = shaka.util.StringUtils.fromUTF16(
request.body, true /* littleEndian */);
var dom = new DOMParser().parseFromString(xml, 'application/xml');
// Set request headers.
var headers = dom.getElementsByTagName('HttpHeader');
for (var i = 0; i < headers.length; ++i) {
var name = headers[i].querySelector('name');
var value = headers[i].querySelector('value');
goog.asserts.assert(name && value, 'Malformed PlayReady headers!');
request.headers[name.textContent] = value.textContent;
}
// Unpack the base64-encoded challenge.
var challenge = dom.querySelector('Challenge');
goog.asserts.assert(challenge, 'Malformed PlayReady challenge!');
goog.asserts.assert(challenge.getAttribute('encoding') == 'base64encoded',
'Unexpected PlayReady challenge encoding!');
request.body =
shaka.util.Uint8ArrayUtils.fromBase64(challenge.textContent).buffer;
};
/**
* @param {!Event} event
* @private
* @suppress {invalidCasts} to swap keyId and status
*/
shaka.media.DrmEngine.prototype.onKeyStatusesChange_ = function(event) {
var session = /** @type {!MediaKeySession} */(event.target);
// Locate the session in the active sessions list.
var i;
for (i = 0; i < this.activeSessions_.length; ++i) {
if (this.activeSessions_[i].session == session) {
break;
}
}
goog.asserts.assert(i < this.activeSessions_.length,
'Key status change for inactive session!');
if (i == this.activeSessions_.length) return;
var keyStatusMap = session.keyStatuses;
var hasExpiredKeys = false;
keyStatusMap.forEach(function(status, keyId) {
// The spec has changed a few times on the exact order of arguments here.
// As of 2016-06-30, Edge has the order reversed compared to the current
// EME spec. Given the back and forth in the spec, it may not be the only
// one. Try to detect this and compensate:
if (typeof keyId == 'string') {
var tmp = keyId;
keyId = /** @type {ArrayBuffer} */(status);
status = /** @type {string} */(tmp);
}
// Microsoft's implementation in Edge seems to present key IDs as
// little-endian UUIDs, rather than big-endian or just plain array of bytes.
// standard: 6e 5a 1d 26 - 27 57 - 47 d7 - 80 46 ea a5 d1 d3 4b 5a
// on Edge: 26 1d 5a 6e - 57 27 - d7 47 - 80 46 ea a5 d1 d3 4b 5a
// Bug filed: https://goo.gl/gnRSkJ
// NOTE that we skip this if byteLength != 16. This is used for the IE11
// and Edge 12 EME polyfill, which uses single-byte dummy key IDs.
if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready' &&
keyId.byteLength == 16) {
// Read out some fields in little-endian:
var dataView = new DataView(keyId);
var part0 = dataView.getUint32(0, true /* LE */);
var part1 = dataView.getUint16(4, true /* LE */);
var part2 = dataView.getUint16(6, true /* LE */);
// Write it back in big-endian:
dataView.setUint32(0, part0, false /* BE */);
dataView.setUint16(4, part1, false /* BE */);
dataView.setUint16(6, part2, false /* BE */);
}
// Microsoft's implementation in IE11 and Edge seems to never set key
// status to 'usable'. It is stuck forever at 'status-pending'. In spite
// of this, the keys do seem to be usable and content plays correctly.
// Bug filed: https://goo.gl/fcXEy1
if (this.currentDrmInfo_.keySystem == 'com.microsoft.playready' &&
status == 'status-pending') {
status = 'usable';
}
if (status != 'status-pending') {
this.activeSessions_[i].loaded = true;
if (this.activeSessions_.every(function(s) { return s.loaded; }))
this.allSessionsLoaded_.resolve();
}
if (status == 'expired') {
hasExpiredKeys = true;
}
var keyIdHex = shaka.util.Uint8ArrayUtils.toHex(new Uint8Array(keyId));
this.keyStatusByKeyId_[keyIdHex] = status;
}.bind(this));
// If the session has expired, close it.
// Some CDMs do not have sub-second time resolution, so the key status may
// fire with hundreds of milliseconds left until the stated expiration time.
var msUntilExpiration = session.expiration - Date.now();
if (msUntilExpiration < 0 || (hasExpiredKeys && msUntilExpiration < 1000)) {
// If this is part of a remove(), we don't want to close the session until
// the update is complete. Otherwise, we will orphan the session.
if (!this.activeSessions_[i].updatePromise) {
shaka.log.debug('Session has expired', session);
this.activeSessions_.splice(i, 1);
session.close();
}
}
// Batch up key status changes before checking them or notifying Player.
// This handles cases where the statuses of multiple sessions are set
// simultaneously by the browser before dispatching key status changes for
// each of them. By batching these up, we only send one status change event
// and at most one EXPIRED error on expiration.
this.keyStatusTimer_.schedule(0.1);
};
/**
* @private
*/
shaka.media.DrmEngine.prototype.processKeyStatusChanges_ = function() {
// If all keys are expired, fire an error.
var allExpired = shaka.util.MapUtils.every(
this.keyStatusByKeyId_, function(keyId, status) {
return status == 'expired';
});
if (allExpired) {
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.EXPIRED));
}
this.onKeyStatus_(this.keyStatusByKeyId_);
};
/**
* Returns true if the browser has recent EME APIs.
*
* @return {boolean}
*/
shaka.media.DrmEngine.isBrowserSupported = function() {
var basic =
!!window.MediaKeys &&
!!window.navigator &&
!!window.navigator.requestMediaKeySystemAccess &&
!!window.MediaKeySystemAccess &&
!!window.MediaKeySystemAccess.prototype.getConfiguration;
return basic;
};
/**
* Returns a Promise to a map of EME support for well-known key systems.
*
* @return {!Promise.<!Object.<string, ?shakaExtern.DrmSupportType>>}
*/
shaka.media.DrmEngine.probeSupport = function() {
goog.asserts.assert(shaka.media.DrmEngine.isBrowserSupported(),
'Must have basic EME support');
var tests = [];
var testKeySystems = [
'org.w3.clearkey',
'com.widevine.alpha',
'com.microsoft.playready',
'com.apple.fps.2_0',
'com.apple.fps.1_0',
'com.apple.fps',
'com.adobe.primetime'
];
var basicVideoCapabilities = [
{ contentType: 'video/mp4; codecs="avc1.42E01E"' },
{ contentType: 'video/webm; codecs="vp8"' }
];
var basicConfig = {
videoCapabilities: basicVideoCapabilities
};
var offlineConfig = {
videoCapabilities: basicVideoCapabilities,
persistentState: 'required',
sessionTypes: ['persistent-license']
};
// Try the offline config first, then fall back to the basic config.
var configs = [offlineConfig, basicConfig];
var support = {};
testKeySystems.forEach(function(keySystem) {
var p = navigator.requestMediaKeySystemAccess(keySystem, configs)
.then(function(access) {
// Edge doesn't return supported session types, but current versions
// do not support persistent-license. If sessionTypes is missing,
// assume no support for persistent-license.
// TODO: polyfill Edge to return known supported session types.
// Edge bug: https://goo.gl/z0URJ0
// Firefox does return supported session types, but will still let you
// create a session even if the type is unsupported.
// Firefox bug: https://goo.gl/lB4H3i
var sessionTypes = access.getConfiguration().sessionTypes;
var persistentState = sessionTypes ?
sessionTypes.indexOf('persistent-license') >= 0 : false;
support[keySystem] = {persistentState: persistentState};
return access.createMediaKeys();
}).catch(function() {
// Either the request failed or createMediaKeys failed.
// Either way, write null to the support object.
support[keySystem] = null;
});
tests.push(p);
});
return Promise.all(tests).then(function() {
return support;
});
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.ManifestParser');
goog.require('goog.Uri');
goog.require('shaka.log');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.Error');
/**
* @namespace shaka.media.ManifestParser
* @summary An interface to register manifest parsers.
* @exportDoc
*/
/**
* Contains the parser factory functions indexed by MIME type.
*
* @type {!Object.<string, shakaExtern.ManifestParser.Factory>}
*/
shaka.media.ManifestParser.parsersByMime = {};
/**
* Contains the parser factory functions indexed by file extension.
*
* @type {!Object.<string, shakaExtern.ManifestParser.Factory>}
*/
shaka.media.ManifestParser.parsersByExtension = {};
/**
* Registers a manifest parser by file extension.
*
* @param {string} extension The file extension of the manifest.
* @param {shakaExtern.ManifestParser.Factory} parserFactory The factory
* used to create parser instances.
* @export
*/
shaka.media.ManifestParser.registerParserByExtension = function(
extension, parserFactory) {
shaka.media.ManifestParser.parsersByExtension[extension] = parserFactory;
};
/**
* Registers a manifest parser by MIME type.
*
* @param {string} mimeType The MIME type of the manifest.
* @param {shakaExtern.ManifestParser.Factory} parserFactory The factory
* used to create parser instances.
* @export
*/
shaka.media.ManifestParser.registerParserByMime = function(
mimeType, parserFactory) {
shaka.media.ManifestParser.parsersByMime[mimeType] = parserFactory;
};
/**
* Returns a map of manifest support for well-known types.
*
* @return {!Object.<string, boolean>}
*/
shaka.media.ManifestParser.probeSupport = function() {
// Make sure all registered parsers are shown.
var support = {};
for (var type in shaka.media.ManifestParser.parsersByMime) {
support[type] = true;
}
for (var type in shaka.media.ManifestParser.parsersByExtension) {
support[type] = true;
}
// Make sure all well-known types are tested as well, just to show an explicit
// false for things people might be expecting.
var testMimeTypes = [
// DASH
'application/dash+xml',
// HLS
'application/x-mpegurl',
'application/vnd.apple.mpegurl',
// SmoothStreaming
'application/vnd.ms-sstr+xml'
];
var testExtensions = [
// DASH
'mpd',
// HLS
'm3u8',
// SmoothStreaming
'ism'
];
testMimeTypes.forEach(function(type) {
support[type] = !!shaka.media.ManifestParser.parsersByMime[type];
});
testExtensions.forEach(function(type) {
support[type] = !!shaka.media.ManifestParser.parsersByExtension[type];
});
return support;
};
/**
* Finds a manifest parser factory to parse the given manifest.
*
* @param {string} manifestUri
* @param {!shaka.net.NetworkingEngine} netEngine
* @param {shakaExtern.RetryParameters} retryParams
* @param {shakaExtern.ManifestParser.Factory=} opt_manifestParserFactory
* @return {!Promise.<shakaExtern.ManifestParser.Factory>}
*/
shaka.media.ManifestParser.getFactory = function(
manifestUri, netEngine, retryParams, opt_manifestParserFactory) {
var factory = opt_manifestParserFactory;
var extension;
if (!factory) {
// Try to choose a manifest parser by file extension.
var uriObj = new goog.Uri(manifestUri);
var uriPieces = uriObj.getPath().split('/');
var uriFilename = uriPieces.pop();
var filenamePieces = uriFilename.split('.');
// Only one piece means there is no extension.
if (filenamePieces.length > 1) {
extension = filenamePieces.pop().toLowerCase();
factory = shaka.media.ManifestParser.parsersByExtension[extension];
}
}
if (factory)
return Promise.resolve(factory);
// Try to choose a manifest parser by MIME type.
var headRequest =
shaka.net.NetworkingEngine.makeRequest([manifestUri], retryParams);
headRequest.method = 'HEAD';
var type = shaka.net.NetworkingEngine.RequestType.MANIFEST;
return netEngine.request(type, headRequest).then(
function(response) {
var mimeType = response.headers['content-type'];
// https://goo.gl/yzKDRx says this header should always be available,
// but just to be safe:
if (mimeType) {
mimeType = mimeType.toLowerCase();
}
factory = shaka.media.ManifestParser.parsersByMime[mimeType];
if (!factory) {
shaka.log.error(
'Unable to guess manifest type by file extension ' +
'or by MIME type.', extension, mimeType);
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.UNABLE_TO_GUESS_MANIFEST_TYPE,
manifestUri));
}
return factory;
}, function(error) {
shaka.log.error('HEAD request to guess manifest type failed!', error);
return Promise.reject(error);
});
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.MediaSourceEngine');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.TextEngine');
goog.require('shaka.media.TimeRangesUtils');
goog.require('shaka.util.Error');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.Functional');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.PublicPromise');
/**
* MediaSourceEngine wraps all operations on MediaSource and SourceBuffers.
* All asynchronous operations return a Promise, and all operations are
* internally synchronized and serialized as needed. Operations that can
* be done in parallel will be done in parallel.
*
* @param {HTMLMediaElement} video The video element, used to read error codes
* when MediaSource operations fail.
* @param {MediaSource} mediaSource The MediaSource, which must be in the
* 'open' state.
* @param {TextTrack} textTrack The TextTrack to use for subtitles/captions.
*
* @struct
* @constructor
* @implements {shaka.util.IDestroyable}
*/
shaka.media.MediaSourceEngine = function(video, mediaSource, textTrack) {
goog.asserts.assert(mediaSource.readyState == 'open',
'The MediaSource should be in the \'open\' state.');
/** @private {HTMLMediaElement} */
this.video_ = video;
/** @private {MediaSource} */
this.mediaSource_ = mediaSource;
/** @private {TextTrack} */
this.textTrack_ = textTrack;
/** @private {!Object.<string, SourceBuffer>} */
this.sourceBuffers_ = {};
/** @private {shaka.media.TextEngine} */
this.textEngine_ = null;
/**
* @private {!Object.<string,
* !Array.<shaka.media.MediaSourceEngine.Operation>>}
*/
this.queues_ = {};
/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/** @private {boolean} */
this.destroyed_ = false;
};
/**
* @typedef {{
* start: function(),
* p: !shaka.util.PublicPromise
* }}
*
* @summary An operation in queue.
* @property {function()} start
* The function which starts the operation.
* @property {!shaka.util.PublicPromise} p
* The PublicPromise which is associated with this operation.
*/
shaka.media.MediaSourceEngine.Operation;
/**
* Checks if a certain type is supported.
*
* @param {string} mimeType
* @return {boolean}
*/
shaka.media.MediaSourceEngine.isTypeSupported = function(mimeType) {
return shaka.media.TextEngine.isTypeSupported(mimeType) ||
MediaSource.isTypeSupported(mimeType);
};
/**
* Returns true if the browser has the basic APIs we need.
*
* @return {boolean}
*/
shaka.media.MediaSourceEngine.isBrowserSupported = function() {
return !!window.MediaSource;
};
/**
* Returns a map of MediaSource support for well-known types.
*
* @return {!Object.<string, boolean>}
*/
shaka.media.MediaSourceEngine.probeSupport = function() {
goog.asserts.assert(shaka.media.MediaSourceEngine.isBrowserSupported(),
'Requires basic support');
var support = {};
var testMimeTypes = [
// MP4 types
'video/mp4; codecs="avc1.42E01E"',
'video/mp4; codecs="avc3.42E01E"',
'video/mp4; codecs="hvc1.1.6.L93.90"',
'audio/mp4; codecs="mp4a.40.2"',
'audio/mp4; codecs="ac-3"',
'audio/mp4; codecs="ec-3"',
// WebM types
'video/webm; codecs="vp8"',
'video/webm; codecs="vp9"',
'video/webm; codecs="av1"',
'audio/webm; codecs="vorbis"',
'audio/webm; codecs="opus"',
// MPEG2 TS types (video/ is also used for audio: http://goo.gl/tYHXiS)
'video/mp2t; codecs="avc1.42E01E"',
'video/mp2t; codecs="avc3.42E01E"',
'video/mp2t; codecs="hvc1.1.6.L93.90"',
'video/mp2t; codecs="mp4a.40.2"',
'video/mp2t; codecs="ac-3"',
'video/mp2t; codecs="ec-3"',
'video/mp2t; codecs="mp4a.40.2"',
// WebVTT types
'text/vtt',
'application/mp4; codecs="wvtt"',
// TTML types
'application/ttml+xml',
'application/mp4; codecs="stpp"'
];
testMimeTypes.forEach(function(type) {
support[type] = shaka.media.MediaSourceEngine.isTypeSupported(type);
var basicType = type.split(';')[0];
support[basicType] = support[basicType] || support[type];
});
return support;
};
/**
* @override
*/
shaka.media.MediaSourceEngine.prototype.destroy = function() {
var Functional = shaka.util.Functional;
this.destroyed_ = true;
var cleanup = [];
for (var contentType in this.queues_) {
// Make a local copy of the queue and the first item.
var q = this.queues_[contentType];
var inProgress = q[0];
// Drop everything else out of the queue.
this.queues_[contentType] = q.slice(0, 1);
// We will wait for this item to complete/fail.
if (inProgress) {
cleanup.push(inProgress.p.catch(Functional.noop));
}
// The rest will be rejected silently if possible.
for (var i = 1; i < q.length; ++i) {
q[i].p.catch(Functional.noop);
q[i].p.reject();
}
}
if (this.textEngine_) {
cleanup.push(this.textEngine_.destroy());
}
return Promise.all(cleanup).then(function() {
this.eventManager_.destroy();
this.eventManager_ = null;
this.video_ = null;
this.mediaSource_ = null;
this.textTrack_ = null;
this.textEngine_ = null;
this.sourceBuffers_ = {};
if (!COMPILED) {
for (var contentType in this.queues_) {
goog.asserts.assert(
this.queues_[contentType].length == 0,
contentType + ' queue should be empty after destroy!');
}
}
this.queues_ = {};
}.bind(this));
};
/**
* @param {!Object.<string, string>} typeConfig A map of content types to full
* MIME types. For example: { 'audio': 'audio/webm; codecs="vorbis"',
* 'video': 'video/webm; codecs="vp9"', 'text': 'text/vtt' }.
* All types given must be supported.
* @param {boolean} useRelativeCueTimestamps
*
* @throws InvalidAccessError if blank MIME types are given
* @throws NotSupportedError if unsupported MIME types are given
* @throws QuotaExceededError if the browser can't support that many buffers
*/
shaka.media.MediaSourceEngine.prototype.init =
function(typeConfig, useRelativeCueTimestamps) {
for (var contentType in typeConfig) {
var mimeType = typeConfig[contentType];
goog.asserts.assert(
shaka.media.MediaSourceEngine.isTypeSupported(mimeType),
'Type negotiation should happen before MediaSourceEngine.init!');
if (contentType == 'text') {
this.textEngine_ =
new shaka.media.TextEngine(this.textTrack_,
mimeType,
useRelativeCueTimestamps);
} else {
var sourceBuffer = this.mediaSource_.addSourceBuffer(mimeType);
this.eventManager_.listen(
sourceBuffer, 'error', this.onError_.bind(this, contentType));
this.eventManager_.listen(
sourceBuffer, 'updateend', this.onUpdateEnd_.bind(this, contentType));
this.sourceBuffers_[contentType] = sourceBuffer;
this.queues_[contentType] = [];
}
}
};
/**
* Gets the first timestamp in buffer for the given content type.
*
* @param {string} contentType
* @return {?number} The timestamp in seconds, or null if nothing is buffered.
*/
shaka.media.MediaSourceEngine.prototype.bufferStart = function(contentType) {
if (contentType == 'text') {
return this.textEngine_.bufferStart();
}
return shaka.media.TimeRangesUtils.bufferStart(
this.getBuffered_(contentType));
};
/**
* Gets the last timestamp in buffer for the given content type.
*
* @param {string} contentType
* @return {?number} The timestamp in seconds, or null if nothing is buffered.
*/
shaka.media.MediaSourceEngine.prototype.bufferEnd = function(contentType) {
if (contentType == 'text') {
return this.textEngine_.bufferEnd();
}
return shaka.media.TimeRangesUtils.bufferEnd(this.getBuffered_(contentType));
};
/**
* Computes how far ahead of the given timestamp is buffered for the given
* content type.
*
* @param {string} contentType
* @param {number} time
* @param {number=} opt_tolerance An optional tolerance for range start times.
* Counts a range starting anywhere from time to time + opt_tolerance.
* @return {number} The amount of time buffered ahead in seconds.
*/
shaka.media.MediaSourceEngine.prototype.bufferedAheadOf =
function(contentType, time, opt_tolerance) {
var bufferedAhead;
if (contentType == 'text') {
bufferedAhead = this.textEngine_.bufferedAheadOf(time);
if (!bufferedAhead && opt_tolerance) {
bufferedAhead = this.textEngine_.bufferedAheadOf(
time + opt_tolerance);
if (bufferedAhead) bufferedAhead += opt_tolerance;
}
} else {
var buffered = this.getBuffered_(contentType);
bufferedAhead = shaka.media.TimeRangesUtils.bufferedAheadOfThreshold(
buffered, time, opt_tolerance || 0);
}
return bufferedAhead;
};
/**
* @param {string} contentType
* @return {TimeRanges} The buffered ranges for the given content type, or
* null if the buffered ranges could not be obtained.
* @private
*/
shaka.media.MediaSourceEngine.prototype.getBuffered_ = function(contentType) {
try {
return this.sourceBuffers_[contentType].buffered;
} catch (exception) {
// Note: previous MediaSource errors may cause access to |buffered| to
// throw.
shaka.log.error('failed to get buffered range for ' + contentType,
exception);
return null;
}
};
/**
* Enqueue an operation to append data to the SourceBuffer.
* Start and end times are needed for TextEngine, but not for MediaSource.
* Start and end times may be null for initialization segments, if present they
* are relative to the presentation timeline.
*
* @param {string} contentType
* @param {!ArrayBuffer} data
* @param {?number} startTime
* @param {?number} endTime
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.appendBuffer =
function(contentType, data, startTime, endTime) {
if (contentType == 'text') {
return this.textEngine_.appendBuffer(data, startTime, endTime);
}
return this.enqueueOperation_(
contentType,
this.append_.bind(this, contentType, data));
};
/**
* Enqueue an operation to remove data from the SourceBuffer.
*
* @param {string} contentType
* @param {number} startTime
* @param {number} endTime
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.remove =
function(contentType, startTime, endTime) {
// On IE11, this operation would be permitted, but would have no effect!
// See https://github.com/google/shaka-player/issues/251
goog.asserts.assert(endTime < Number.MAX_VALUE,
'remove() with MAX_VALUE or Infinity is not IE-compatible!');
if (contentType == 'text') {
return this.textEngine_.remove(startTime, endTime);
}
return this.enqueueOperation_(
contentType,
this.remove_.bind(this, contentType, startTime, endTime));
};
/**
* Enqueue an operation to clear the SourceBuffer.
*
* @param {string} contentType
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.clear = function(contentType) {
if (contentType == 'text') {
return this.textEngine_.remove(0, Infinity);
}
// Note that not all platforms allow clearing to Infinity.
return this.enqueueOperation_(
contentType,
this.remove_.bind(this, contentType, 0, this.mediaSource_.duration));
};
/**
* Enqueue an operation to flush the SourceBuffer.
* This is a workaround for what we believe is a Chromecast bug.
*
* @param {string} contentType
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.flush = function(contentType) {
// Flush the pipeline. Necessary on Chromecast, even though we have removed
// everything.
if (contentType == 'text') {
// Nothing to flush for text.
return Promise.resolve();
}
return this.enqueueOperation_(
contentType,
this.flush_.bind(this, contentType));
};
/**
* Sets the timestamp offset for the given content type.
*
* @param {string} contentType
* @param {number} timestampOffset The timestamp offset. Segments which start
* at time t will be inserted at time t + timestampOffset instead. This
* value does not affect segments which have already been inserted.
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.setTimestampOffset = function(
contentType, timestampOffset) {
if (contentType == 'text') {
this.textEngine_.setTimestampOffset(timestampOffset);
return Promise.resolve();
}
return this.enqueueOperation_(
contentType,
this.setTimestampOffset_.bind(this, contentType, timestampOffset));
};
/**
* Sets the append window end for the given content type.
*
* @param {string} contentType
* @param {number} appendWindowEnd The timestamp to set the append window end
* to. Media beyond this value will be truncated.
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.setAppendWindowEnd = function(
contentType, appendWindowEnd) {
if (contentType == 'text') {
this.textEngine_.setAppendWindowEnd(appendWindowEnd);
return Promise.resolve();
}
return Promise.all([
// Queue an abort() to help MSE splice together overlapping segments.
// We set appendWindowEnd when we change periods in DASH content, and the
// period transition may result in overlap.
this.enqueueOperation_(
contentType,
this.abort_.bind(this, contentType)),
this.enqueueOperation_(
contentType,
this.setAppendWindowEnd_.bind(this, contentType, appendWindowEnd))
]);
};
/**
* @param {string=} opt_reason Valid reasons are 'network' and 'decode'.
* @return {!Promise}
* @see http://w3c.github.io/media-source/#idl-def-EndOfStreamError
*/
shaka.media.MediaSourceEngine.prototype.endOfStream = function(opt_reason) {
return this.enqueueBlockingOperation_(function() {
// Chrome won't let me pass undefined, but it will let me omit the
// argument. Firefox does not have this problem.
// TODO: File a bug about this.
if (opt_reason) {
this.mediaSource_.endOfStream(opt_reason);
} else {
this.mediaSource_.endOfStream();
}
}.bind(this));
};
/**
* We only support increasing duration at this time. Decreasing duration
* causes the MSE removal algorithm to run, which results in an 'updateend'
* event. Supporting this scenario would be complicated, and is not currently
* needed.
*
* @param {number} duration
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.setDuration = function(duration) {
goog.asserts.assert(
isNaN(this.mediaSource_.duration) ||
this.mediaSource_.duration <= duration,
'duration cannot decrease: ' + this.mediaSource_.duration + ' -> ' +
duration);
return this.enqueueBlockingOperation_(function() {
this.mediaSource_.duration = duration;
}.bind(this));
};
/**
* Get the current MediaSource duration.
*
* @return {number}
*/
shaka.media.MediaSourceEngine.prototype.getDuration = function() {
return this.mediaSource_.duration;
};
/**
* Append data to the SourceBuffer.
* @param {string} contentType
* @param {!ArrayBuffer} data
* @throws QuotaExceededError if the browser's buffer is full
* @private
*/
shaka.media.MediaSourceEngine.prototype.append_ =
function(contentType, data) {
// This will trigger an 'updateend' event.
this.sourceBuffers_[contentType].appendBuffer(data);
};
/**
* Remove data from the SourceBuffer.
* @param {string} contentType
* @param {number} startTime
* @param {number} endTime
* @private
*/
shaka.media.MediaSourceEngine.prototype.remove_ =
function(contentType, startTime, endTime) {
if (endTime <= startTime) {
// Ignore removal of inverted or empty ranges.
// Fake 'updateend' event to resolve the operation.
this.onUpdateEnd_(contentType);
return;
}
// This will trigger an 'updateend' event.
this.sourceBuffers_[contentType].remove(startTime, endTime);
};
/**
* Call abort() on the SourceBuffer.
* This resets MSE's last_decode_timestamp on all track buffers, which should
* trigger the splicing logic for overlapping segments.
* @param {string} contentType
* @private
*/
shaka.media.MediaSourceEngine.prototype.abort_ = function(contentType) {
// Save the append window end, which is reset on abort().
var appendWindowEnd = this.sourceBuffers_[contentType].appendWindowEnd;
// This will not trigger an 'updateend' event, since nothing is happening.
// This is only to reset MSE internals, not to abort an actual operation.
this.sourceBuffers_[contentType].abort();
// Restore the append window end.
this.sourceBuffers_[contentType].appendWindowEnd = appendWindowEnd;
// Fake 'updateend' event to resolve the operation.
this.onUpdateEnd_(contentType);
};
/**
* Nudge the playhead to force the media pipeline to be flushed.
* This seems to be necessary on Chromecast to get new content to replace old
* content.
* @param {string} contentType
* @private
*/
shaka.media.MediaSourceEngine.prototype.flush_ = function(contentType) {
// Never use flush_ if there's data. It causes a hiccup in playback.
goog.asserts.assert(
this.video_.buffered.length == 0,
'MediaSourceEngine.flush_ should only be used after clearing all data!');
// Seeking forces the pipeline to be flushed.
this.video_.currentTime -= 0.001;
// Fake 'updateend' event to resolve the operation.
this.onUpdateEnd_(contentType);
};
/**
* Set the SourceBuffer's timestamp offset.
* @param {string} contentType
* @param {number} timestampOffset
* @private
*/
shaka.media.MediaSourceEngine.prototype.setTimestampOffset_ =
function(contentType, timestampOffset) {
this.sourceBuffers_[contentType].timestampOffset = timestampOffset;
// Fake 'updateend' event to resolve the operation.
this.onUpdateEnd_(contentType);
};
/**
* Set the SourceBuffer's append window end.
* @param {string} contentType
* @param {number} appendWindowEnd
* @private
*/
shaka.media.MediaSourceEngine.prototype.setAppendWindowEnd_ =
function(contentType, appendWindowEnd) {
var fudge = 1 / 25; // one frame, assuming a low framerate
this.sourceBuffers_[contentType].appendWindowEnd = appendWindowEnd + fudge;
// Fake 'updateend' event to resolve the operation.
this.onUpdateEnd_(contentType);
};
/**
* @param {string} contentType
* @param {!Event} event
* @private
*/
shaka.media.MediaSourceEngine.prototype.onError_ =
function(contentType, event) {
var operation = this.queues_[contentType][0];
goog.asserts.assert(operation, 'Spurious error event!');
goog.asserts.assert(!this.sourceBuffers_[contentType].updating,
'SourceBuffer should not be updating on error!');
var code = this.video_.error ? this.video_.error.code : 0;
operation.p.reject(new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_FAILED,
code));
// Do not pop from queue. An 'updateend' event will fire next, and to avoid
// synchronizing these two event handlers, we will allow that one to pop from
// the queue as normal. Note that because the operation has already been
// rejected, the call to resolve() in the 'updateend' handler will have no
// effect.
};
/**
* @param {string} contentType
* @private
*/
shaka.media.MediaSourceEngine.prototype.onUpdateEnd_ = function(contentType) {
var operation = this.queues_[contentType][0];
goog.asserts.assert(operation, 'Spurious updateend event!');
if (!operation) return;
goog.asserts.assert(!this.sourceBuffers_[contentType].updating,
'SourceBuffer should not be updating on updateend!');
operation.p.resolve();
this.popFromQueue_(contentType);
};
/**
* Enqueue an operation and start it if appropriate.
*
* @param {string} contentType
* @param {function()} start
* @return {!Promise}
* @private
*/
shaka.media.MediaSourceEngine.prototype.enqueueOperation_ =
function(contentType, start) {
if (this.destroyed_) return Promise.reject();
var operation = {
start: start,
p: new shaka.util.PublicPromise()
};
this.queues_[contentType].push(operation);
if (this.queues_[contentType].length == 1) {
try {
operation.start();
} catch (exception) {
if (exception.name == 'QuotaExceededError') {
operation.p.reject(new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR,
contentType));
} else {
operation.p.reject(new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
exception));
}
this.popFromQueue_(contentType);
}
}
return operation.p;
};
/**
* Enqueue an operation which must block all other operations on all
* SourceBuffers.
*
* @param {function()} run
* @return {!Promise}
* @private
*/
shaka.media.MediaSourceEngine.prototype.enqueueBlockingOperation_ =
function(run) {
if (this.destroyed_) return Promise.reject();
var allWaiters = [];
// Enqueue a 'wait' operation onto each queue.
// This operation signals its readiness when it starts.
// When all wait operations are ready, the real operation takes place.
for (var contentType in this.sourceBuffers_) {
var ready = new shaka.util.PublicPromise();
var operation = {
start: function(ready) { ready.resolve(); }.bind(null, ready),
p: ready
};
this.queues_[contentType].push(operation);
allWaiters.push(ready);
if (this.queues_[contentType].length == 1) {
operation.start();
}
}
// Return a Promise to the real operation, which waits to begin until there
// are no other in-progress operations on any SourceBuffers.
return Promise.all(allWaiters).then(function() {
if (!COMPILED) {
// If we did it correctly, nothing is updating.
for (var contentType in this.sourceBuffers_) {
goog.asserts.assert(
this.sourceBuffers_[contentType].updating == false,
'SourceBuffers should not be updating after a blocking op!');
}
}
var ret;
// Run the real operation, which is synchronous.
try {
run();
} catch (exception) {
ret = Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
exception));
}
// Unblock the queues.
for (var contentType in this.sourceBuffers_) {
this.popFromQueue_(contentType);
}
return ret;
}.bind(this), function() {
// One of the waiters failed, which means we've been destroyed.
goog.asserts.assert(this.destroyed_, 'Should be destroyed by now');
// We haven't popped from the queue. Canceled waiters have been removed by
// destroy. What's left now should just be resolved waiters. In uncompiled
// mode, we will maintain good hygiene and make sure the assert at the end
// of destroy passes. In compiled mode, the queues are wiped in destroy.
if (!COMPILED) {
for (var contentType in this.sourceBuffers_) {
if (this.queues_[contentType].length) {
goog.asserts.assert(
this.queues_[contentType].length == 1,
'Should be at most one item in queue!');
goog.asserts.assert(
allWaiters.indexOf(this.queues_[contentType][0].p) != -1,
'The item in queue should be one of our waiters!');
this.queues_[contentType].shift();
}
}
}
return Promise.reject();
}.bind(this));
};
/**
* Pop from the front of the queue and start a new operation.
* @param {string} contentType
* @private
*/
shaka.media.MediaSourceEngine.prototype.popFromQueue_ = function(contentType) {
// Remove the in-progress operation, which is now complete.
this.queues_[contentType].shift();
// Retrieve the next operation, if any, from the queue and start it.
var next = this.queues_[contentType][0];
if (next) {
try {
next.start();
} catch (exception) {
next.p.reject(new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
exception));
this.popFromQueue_(contentType);
}
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.Mp4SegmentIndexParser');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.util.DataViewReader');
goog.require('shaka.util.Error');
goog.require('shaka.util.Mp4Parser');
/**
* Parses SegmentReferences from an ISO BMFF SIDX structure.
* @param {!ArrayBuffer} sidxData The MP4's container's SIDX.
* @param {number} sidxOffset The SIDX's offset, in bytes, from the start of
* the MP4 container.
* @param {!Array.<string>} uris The possible locations of the MP4 file that
* contains the segments.
* @param {number} presentationTimeOffset
* @return {!Array.<!shaka.media.SegmentReference>}
* @throws {shaka.util.Error}
*/
shaka.media.Mp4SegmentIndexParser = function(
sidxData, sidxOffset, uris, presentationTimeOffset) {
var references = [];
var reader = new shaka.util.DataViewReader(
new DataView(sidxData),
shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
var boxSize = shaka.util.Mp4Parser.findBox(
shaka.media.Mp4SegmentIndexParser.BOX_TYPE, reader);
if (boxSize == shaka.util.Mp4Parser.BOX_NOT_FOUND) {
shaka.log.error('Invalid box type, expected "sidx".');
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.MP4_SIDX_WRONG_BOX_TYPE);
}
// Parse the FullBox structure.
var version = reader.readUint8();
// Skip flags (24 bits)
reader.skip(3);
// Parse the SIDX structure.
// Skip reference_ID (32 bits).
reader.skip(4);
var timescale = reader.readUint32();
goog.asserts.assert(timescale != 0, 'timescale cannot be 0');
if (timescale == 0) {
shaka.log.error('Invalid timescale.');
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.MP4_SIDX_INVALID_TIMESCALE);
}
var earliestPresentationTime;
var firstOffset;
if (version == 0) {
earliestPresentationTime = reader.readUint32();
firstOffset = reader.readUint32();
} else {
earliestPresentationTime = reader.readUint64();
firstOffset = reader.readUint64();
}
// Skip reserved (16 bits).
reader.skip(2);
// Add references.
var referenceCount = reader.readUint16();
// Substract the presentationTimeOffset
var unscaledStartTime = earliestPresentationTime - presentationTimeOffset;
var startByte = sidxOffset + boxSize + firstOffset;
for (var i = 0; i < referenceCount; i++) {
// |chunk| is 1 bit for |referenceType|, and 31 bits for |referenceSize|.
var chunk = reader.readUint32();
var referenceType = (chunk & 0x80000000) >>> 31;
var referenceSize = chunk & 0x7FFFFFFF;
var subsegmentDuration = reader.readUint32();
// Skipping 1 bit for |startsWithSap|, 3 bits for |sapType|, and 28 bits
// for |sapDelta|.
reader.skip(4);
// If |referenceType| is 1 then the reference is to another SIDX.
// We do not support this.
if (referenceType == 1) {
shaka.log.error('Heirarchical SIDXs are not supported.');
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.MP4_SIDX_TYPE_NOT_SUPPORTED);
}
references.push(
new shaka.media.SegmentReference(
references.length,
unscaledStartTime / timescale,
(unscaledStartTime + subsegmentDuration) / timescale,
function() { return uris; },
startByte,
startByte + referenceSize - 1));
unscaledStartTime += subsegmentDuration;
startByte += referenceSize;
}
return references;
};
/**
* Indicates the SIDX box structure. It is equal to the string 'sidx' as a
* 32-bit unsigned integer.
* @const {number}
*/
shaka.media.Mp4SegmentIndexParser.BOX_TYPE = 0x73696478;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.Mp4TtmlParser');
goog.require('shaka.media.TextEngine');
goog.require('shaka.media.TtmlTextParser');
goog.require('shaka.util.DataViewReader');
goog.require('shaka.util.Error');
goog.require('shaka.util.Mp4Parser');
/**
* @namespace
* @summary Extracts a TTML segment from an MP4 file and invokes the TTML parser
* to parse it.
* @param {ArrayBuffer} data
* @param {number} offset
* @param {?number} segmentStartTime
* @param {?number} segmentEndTime
* @param {boolean} useRelativeCueTimestamps Only used by the VTT parser
* @return {!Array.<!TextTrackCue>}
* @export
*/
shaka.media.Mp4TtmlParser =
function(data, offset, segmentStartTime,
segmentEndTime, useRelativeCueTimestamps) {
var reader = new shaka.util.DataViewReader(
new DataView(data),
shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
var boxSize = shaka.util.Mp4Parser.findBox(
shaka.util.Mp4Parser.BOX_TYPE_MDAT, reader);
if (boxSize != shaka.util.Mp4Parser.BOX_NOT_FOUND) {
// mdat box found, use TtmlTextParser to parse the content
return shaka.media.TtmlTextParser(
reader.readBytes(boxSize - 8).buffer, offset,
segmentStartTime, segmentEndTime, false);
}
var stppBoxSize = shaka.util.Mp4Parser.findSampleDescriptionBox(
data, shaka.media.Mp4TtmlParser.BOX_TYPE_STPP);
if (stppBoxSize != shaka.util.Mp4Parser.BOX_NOT_FOUND) {
// a valid ttml init segment, no actual subtitles yet
return [];
} else {
throw new shaka.util.Error(
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_MP4_TTML);
}
};
/** @const {number} */
shaka.media.Mp4TtmlParser.BOX_TYPE_STPP = 0x73747070;
shaka.media.TextEngine.registerParser(
'application/mp4; codecs="stpp"', shaka.media.Mp4TtmlParser);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.Mp4VttParser');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.TextEngine');
goog.require('shaka.media.VttTextParser');
goog.require('shaka.util.DataViewReader');
goog.require('shaka.util.Error');
goog.require('shaka.util.Mp4Parser');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.TextParser');
/**
* @namespace
* @summary Extracts a VTT segment from an MP4 file and maps it to cue objects.
* @param {ArrayBuffer} data
* @param {number} offset
* @param {?number} segmentStartTime
* @param {?number} segmentEndTime
* @param {boolean} useRelativeCueTimestamps Only used by the VTT parser
* @return {!Array.<!TextTrackCue>}
* @export
*/
shaka.media.Mp4VttParser =
function(data, offset, segmentStartTime,
segmentEndTime, useRelativeCueTimestamps) {
var reader = new shaka.util.DataViewReader(
new DataView(data),
shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
var boxSize = shaka.util.Mp4Parser.findBox(
shaka.util.Mp4Parser.BOX_TYPE_MDAT, reader);
if (boxSize != shaka.util.Mp4Parser.BOX_NOT_FOUND) {
// mdat box found, parse the content
// valid media segment should have start and end time
goog.asserts.assert(
segmentStartTime != null, 'start time should not be null');
goog.asserts.assert(segmentEndTime != null, 'end time should not be null');
return shaka.media.Mp4VttParser.parseData_(
reader.readBytes(boxSize - 8).buffer, offset,
segmentStartTime, segmentEndTime);
}
var wvttBoxSize = shaka.util.Mp4Parser.findSampleDescriptionBox(
data, shaka.media.Mp4VttParser.BOX_TYPE_WVTT);
if (wvttBoxSize != shaka.util.Mp4Parser.BOX_NOT_FOUND) {
// a valid vtt init segment, no actual subtitles yet
return [];
} else {
throw new shaka.util.Error(
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_MP4_VTT);
}
};
/**
* Parses the content of the mdat MP4 box into cue objects.
*
* @param {ArrayBuffer} data
* @param {number} offset
* @param {number} segmentStartTime
* @param {number} segmentEndTime
* @return {!Array.<!TextTrackCue>}
* @private
*/
shaka.media.Mp4VttParser.parseData_ = function(
data, offset, segmentStartTime, segmentEndTime) {
var reader = new shaka.util.DataViewReader(
new DataView(data),
shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
segmentStartTime += offset;
segmentEndTime += offset;
var result = [];
// Cues are represented as vttc boxes. Each box corresponds to a cue.
while (reader.hasMoreData()) {
var boxSize = shaka.util.Mp4Parser.findBox(
shaka.media.Mp4VttParser.BOX_TYPE_VTTC, reader);
if (boxSize == shaka.util.Mp4Parser.BOX_NOT_FOUND) {
// No more cues
break;
}
var cue = shaka.media.Mp4VttParser.parseCue_(
reader.readBytes(boxSize - 8).buffer,
segmentStartTime, segmentEndTime);
if (cue)
result.push(cue);
}
return result;
};
/**
* Parses a vttc box into a cue.
*
* @param {ArrayBuffer} data
* @param {number} segmentStartTime
* @param {number} segmentEndTime
* @return {TextTrackCue}
* @private
*/
shaka.media.Mp4VttParser.parseCue_ = function(
data, segmentStartTime, segmentEndTime) {
var reader = new shaka.util.DataViewReader(
new DataView(data),
shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
var payload;
var settings;
var id;
while (reader.hasMoreData()) {
var startPosition = reader.getPosition();
var size = reader.readUint32();
var type = reader.readUint32();
var content = shaka.util.StringUtils.fromUTF8(
reader.readBytes(size - 8).buffer);
if (size == 1) {
size = reader.readUint64();
} else if (size == 0) {
size = reader.getLength() - startPosition;
}
switch (type) {
case shaka.media.Mp4VttParser.BOX_TYPE_PAYL:
payload = content;
break;
case shaka.media.Mp4VttParser.BOX_TYPE_IDEN:
id = content;
break;
case shaka.media.Mp4VttParser.BOX_TYPE_STTG:
settings = content;
break;
default:
break;
}
}
// payload box is mandatory
if (!payload) {
throw new shaka.util.Error(
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_MP4_VTT);
}
var cue = shaka.media.TextEngine.makeCue(
segmentStartTime, segmentEndTime, payload);
if (!cue)
return null;
if (id)
cue.id = id;
if (settings) {
var parser = new shaka.util.TextParser(settings);
var word = parser.readWord();
while (word) {
if (!shaka.media.VttTextParser.parseSetting(cue, word)) {
shaka.log.warning('VTT parser encountered an invalid VTT setting: ',
word,
' The setting will be ignored.');
}
parser.skipWhitespace();
word = parser.readWord();
}
}
return cue;
};
/** @const {number} */
shaka.media.Mp4VttParser.BOX_TYPE_WVTT = 0x77767474;
/** @const {number} */
shaka.media.Mp4VttParser.BOX_TYPE_VTTC = 0x76747463;
/** @const {number} */
shaka.media.Mp4VttParser.BOX_TYPE_PAYL = 0x7061796C;
/** @const {number} */
shaka.media.Mp4VttParser.BOX_TYPE_IDEN = 0x6964656F;
/** @const {number} */
shaka.media.Mp4VttParser.BOX_TYPE_STTG = 0x73747467;
shaka.media.TextEngine.registerParser(
'application/mp4; codecs="wvtt"', shaka.media.Mp4VttParser);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.Playhead');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.PresentationTimeline');
goog.require('shaka.media.TimeRangesUtils');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.IDestroyable');
/**
* Creates a Playhead, which manages the video's current time.
*
* The Playhead provides mechanisms for setting the presentation's start time,
* restricting seeking to valid time ranges, and stopping playback for startup
* and re- buffering.
*
* @param {HTMLMediaElement} video
* @param {!shaka.media.PresentationTimeline} timeline
* @param {number} rebufferingGoal
* @param {?number} startTime The playhead's initial position in seconds. If
* null, defaults to the start of the presentation for VOD and the live-edge
* for live.
* @param {function(boolean)} onBuffering Called and passed true when stopped
* for buffering; called and passed false when proceeding after buffering.
* If passed true, the callback should not set the video's playback rate.
* @param {function()} onSeek Called when the user agent seeks to a time within
* the presentation timeline.
*
* @constructor
* @struct
* @implements {shaka.util.IDestroyable}
*/
shaka.media.Playhead = function(
video, timeline, rebufferingGoal, startTime, onBuffering, onSeek) {
/** @private {HTMLMediaElement} */
this.video_ = video;
/** @private {shaka.media.PresentationTimeline} */
this.timeline_ = timeline;
/** @private {number} */
this.rebufferingGoal_ = rebufferingGoal;
/**
* The playhead's initial position in seconds, or null if it should
* automatically be calculated later.
* @private {?number}
* @const
*/
this.startTime_ = startTime;
/** @private {?function(boolean)} */
this.onBuffering_ = onBuffering;
/** @private {?function()} */
this.onSeek_ = onSeek;
/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/** @private {boolean} */
this.buffering_ = false;
/** @private {number} */
this.playbackRate_ = 1;
/** @private {?number} */
this.trickPlayIntervalId_ = null;
/** @private {?number} */
this.watchdogTimer_ = null;
// Check if the video has already loaded some metadata.
if (video.readyState > 0) {
this.onLoadedMetadata_();
} else {
this.eventManager_.listen(
video, 'loadedmetadata', this.onLoadedMetadata_.bind(this));
}
this.eventManager_.listen(video, 'ratechange', this.onRateChange_.bind(this));
this.startWatchdogTimer_();
};
/** @override */
shaka.media.Playhead.prototype.destroy = function() {
var p = this.eventManager_.destroy();
this.eventManager_ = null;
this.cancelWatchdogTimer_();
if (this.trickPlayIntervalId_ != null) {
window.clearInterval(this.trickPlayIntervalId_);
this.trickPlayIntervalId_ = null;
}
this.video_ = null;
this.timeline_ = null;
this.onBuffering_ = null;
this.onSeek_ = null;
return p;
};
/** @param {number} rebufferingGoal */
shaka.media.Playhead.prototype.setRebufferingGoal = function(rebufferingGoal) {
this.rebufferingGoal_ = rebufferingGoal;
};
/**
* Gets the playhead's current (logical) position.
*
* @return {number}
*/
shaka.media.Playhead.prototype.getTime = function() {
if (this.video_.readyState > 0) {
// Although we restrict the video's currentTime elsewhere, clamp it here to
// ensure any timing issues (e.g., the user agent seeks and calls this
// function before we receive the 'seeking' event) don't cause us to return
// a time outside the segment availability window.
return this.clampTime_(this.video_.currentTime);
}
return this.getStartTime_();
};
/**
* Gets the playhead's initial position in seconds.
*
* @return {number}
* @private
*/
shaka.media.Playhead.prototype.getStartTime_ = function() {
if (this.startTime_) {
return this.clampTime_(this.startTime_);
}
var startTime;
if (this.timeline_.getDuration() < Infinity) {
// If the presentation is VOD, or if the presentation is live but has
// finished broadcasting, then start from the beginning.
startTime = this.timeline_.getEarliestStart();
} else {
// Otherwise, start near the live-edge, but ensure that the startup
// buffering goal can be met
startTime = Math.max(
this.timeline_.getSeekRangeEnd(),
this.timeline_.getEarliestStart());
}
return startTime;
};
/**
* Stops the playhead for buffering, or resumes the playhead after buffering.
*
* @param {boolean} buffering True to stop the playhead; false to allow it to
* continue.
*/
shaka.media.Playhead.prototype.setBuffering = function(buffering) {
if (buffering != this.buffering_) {
this.buffering_ = buffering;
this.setPlaybackRate(this.playbackRate_);
this.onBuffering_(buffering);
}
};
/**
* Starts the watchdog timer.
* @private
*/
shaka.media.Playhead.prototype.startWatchdogTimer_ = function() {
this.cancelWatchdogTimer_();
this.watchdogTimer_ =
window.setTimeout(this.onWatchdogTimer_.bind(this), 250);
};
/**
* Cancels the watchdog timer, if any.
* @private
*/
shaka.media.Playhead.prototype.cancelWatchdogTimer_ = function() {
if (this.watchdogTimer_) {
window.clearTimeout(this.watchdogTimer_);
this.watchdogTimer_ = null;
}
};
/**
* Called on a recurring timer to detect buffering events.
* @private
*/
shaka.media.Playhead.prototype.onWatchdogTimer_ = function() {
this.watchdogTimer_ = null;
this.startWatchdogTimer_();
// This uses an intersection of buffered ranges for both audio and video, so
// it's an accurate way to determine if we are buffering or not.
var bufferedAhead = shaka.media.TimeRangesUtils.bufferedAheadOfThreshold(
this.video_.buffered, this.video_.currentTime, 0.1);
var bufferEnd = shaka.media.TimeRangesUtils.bufferEnd(this.video_.buffered);
var fudgeFactor = shaka.media.Playhead.FUDGE_FACTOR_;
var threshold = shaka.media.Playhead.UNDERFLOW_THRESHOLD_;
var duration;
if (this.timeline_.isLive()) {
duration = this.timeline_.getSegmentAvailabilityEnd() - fudgeFactor;
} else {
duration = this.video_.duration - fudgeFactor;
}
var atEnd = (bufferEnd >= duration) || (this.video_.ended);
if (!this.buffering_) {
// If there are no buffered ranges but the playhead is at the end of
// the video then we shouldn't enter a buffering state.
if (!atEnd && bufferedAhead < threshold) {
this.setBuffering(true);
}
} else {
if (atEnd || bufferedAhead >= this.rebufferingGoal_) {
this.setBuffering(false);
}
}
};
/**
* Gets the current effective playback rate. This may be negative even if the
* browser does not directly support rewinding.
* @return {number}
*/
shaka.media.Playhead.prototype.getPlaybackRate = function() {
return this.playbackRate_;
};
/**
* Sets the playback rate.
* @param {number} rate
*/
shaka.media.Playhead.prototype.setPlaybackRate = function(rate) {
if (this.trickPlayIntervalId_ != null) {
window.clearInterval(this.trickPlayIntervalId_);
this.trickPlayIntervalId_ = null;
}
this.playbackRate_ = rate;
// All major browsers support playback rates above zero. Only need fake
// trick play for negative rates.
this.video_.playbackRate = (this.buffering_ || rate < 0) ? 0 : rate;
if (!this.buffering_ && rate < 0) {
// Defer creating the timer until we stop buffering. This function will be
// called again from setBuffering().
this.trickPlayIntervalId_ = window.setInterval(function() {
this.video_.currentTime += rate / 4;
}.bind(this), 250);
}
};
/**
* Handles a 'ratechange' event.
*
* @private
*/
shaka.media.Playhead.prototype.onRateChange_ = function() {
// NOTE: This will not allow explicitly setting the playback rate to 0 while
// the playback rate is negative. Pause will still work.
var expectedRate =
this.buffering_ || this.playbackRate_ < 0 ? 0 : this.playbackRate_;
if (this.video_.playbackRate != expectedRate) {
shaka.log.debug('Video playback rate changed to', this.video_.playbackRate);
this.setPlaybackRate(this.video_.playbackRate);
}
};
/**
* Handles a 'loadedmetadata' event.
*
* @private
*/
shaka.media.Playhead.prototype.onLoadedMetadata_ = function() {
this.eventManager_.unlisten(this.video_, 'loadedmetadata');
// Move the real playhead to the start time.
var targetTime = this.getStartTime_();
if (Math.abs(this.video_.currentTime - targetTime) < 0.001) {
this.eventManager_.listen(
this.video_, 'seeking', this.onSeeking_.bind(this));
this.eventManager_.listen(
this.video_, 'playing', this.onPlaying_.bind(this));
} else {
this.eventManager_.listen(
this.video_, 'seeking', this.onSeekingToStartTime_.bind(this));
this.video_.currentTime = targetTime;
}
};
/**
* Handles the 'seeking' event from the initial jump to the start time (if
* there is one).
*
* @private
*/
shaka.media.Playhead.prototype.onSeekingToStartTime_ = function() {
goog.asserts.assert(this.video_.readyState > 0,
'readyState should be greater than 0');
this.eventManager_.unlisten(this.video_, 'seeking');
this.eventManager_.listen(this.video_, 'seeking', this.onSeeking_.bind(this));
this.eventManager_.listen(this.video_, 'playing', this.onPlaying_.bind(this));
};
/**
* Handles a 'seeking' event.
*
* @private
*/
shaka.media.Playhead.prototype.onSeeking_ = function() {
goog.asserts.assert(this.video_.readyState > 0,
'readyState should be greater than 0');
var currentTime = this.video_.currentTime;
var targetTime = this.reposition_(currentTime);
if (Math.abs(targetTime - currentTime) > 0.001) {
this.movePlayhead_(currentTime, targetTime);
return;
}
shaka.log.v1('Seek to ' + currentTime);
this.onSeek_();
};
/**
* Handles a 'playing' event.
*
* @private
*/
shaka.media.Playhead.prototype.onPlaying_ = function() {
goog.asserts.assert(this.video_.readyState > 0,
'readyState should be greater than 0');
var currentTime = this.video_.currentTime;
var targetTime = this.reposition_(currentTime);
if (Math.abs(targetTime - currentTime) > 0.001)
this.movePlayhead_(currentTime, targetTime);
};
/**
* Computes a new playhead position that's within the presentation timeline.
*
* @param {number} currentTime
* @return {number} The time to reposition the playhead to.
* @private
*/
shaka.media.Playhead.prototype.reposition_ = function(currentTime) {
var timeline = this.timeline_;
var start = timeline.getEarliestStart();
var end = timeline.getSegmentAvailabilityEnd();
if (!timeline.isLive() ||
timeline.getSegmentAvailabilityDuration() == Infinity) {
// If the presentation is live but has an infinite segment availability
// duration then we can treat it as VOD since the start of the window is
// not moving.
if (currentTime < start) {
shaka.log.v1('Playhead before start.');
return start;
} else if (currentTime > end) {
shaka.log.v1('Playhead past end.');
return end;
}
return currentTime;
}
// TODO: Link to public doc that explains the following code.
var left = start + 1;
var safe = left + this.rebufferingGoal_;
if (currentTime >= safe && currentTime <= end) {
shaka.log.v1('Playhead in safe region.');
return currentTime;
}
var bufferedAhead = shaka.media.TimeRangesUtils.bufferedAheadOf(
this.video_.buffered, currentTime);
if ((bufferedAhead != 0) && (currentTime >= left && currentTime <= end)) {
shaka.log.v1('Playhead outside safe region & in buffered region.');
return currentTime;
} else if (currentTime > end) {
shaka.log.v1('Playhead past end.');
return end;
} else if ((end < safe) && (currentTime >= left && currentTime <= end)) {
// The segment availability window is so small we cannot reposition the
// playhead normally; however, since |currentTime| is within the window, we
// don't have to do anything.
shaka.log.v1('Playhead outside safe region & in unbuffered region,',
'but cannot reposition the playhead.');
return currentTime;
}
// It's not safe to buffer from |currentTime|, so reposition the playhead.
shaka.log.v1('Playhead outside safe region & in unbuffered region,',
'or playhead before start');
return Math.min(safe + 2, end);
};
/**
* Moves the playhead to the target time, triggering a call to onSeeking_().
*
* @param {number} currentTime
* @param {number} targetTime
* @private
*/
shaka.media.Playhead.prototype.movePlayhead_ = function(
currentTime, targetTime) {
shaka.log.debug('Moving playhead...',
'currentTime=' + currentTime,
'targetTime=' + targetTime);
this.video_.currentTime = targetTime;
// Sometimes, IE and Edge ignore re-seeks. Check every 100ms and try
// again if need be, up to 10 tries.
// Delay stats over 100 runs of a re-seeking integration test:
// IE - 0ms - 47%
// IE - 100ms - 63%
// Edge - 0ms - 2%
// Edge - 100ms - 40%
// Edge - 200ms - 32%
// Edge - 300ms - 24%
// Edge - 400ms - 2%
// Chrome - 0ms - 100%
// TODO: File a bug on IE/Edge about this.
var tries = 0;
var recheck = (function() {
if (!this.video_) return;
if (tries++ >= 10) return;
if (this.video_.currentTime == currentTime) {
// Sigh. Try again.
this.video_.currentTime = targetTime;
setTimeout(recheck, 100);
}
}).bind(this);
setTimeout(recheck, 100);
};
/**
* Clamps the given time to the segment availability window.
*
* @param {number} time The time in seconds.
* @return {number} The clamped time in seconds.
* @private
*/
shaka.media.Playhead.prototype.clampTime_ = function(time) {
var start = this.timeline_.getEarliestStart();
if (time < start) return start;
var end = this.timeline_.getSegmentAvailabilityEnd();
if (time > end) return end;
return time;
};
/**
* The threshold for underflow, in seconds. If there is less than this amount
* of data buffered, we will consider the player to be out of data.
*
* @private {number}
* @const
*/
shaka.media.Playhead.UNDERFLOW_THRESHOLD_ = 0.5;
/**
* A fudge factor used when comparing buffered ranges to the duration to
* determine if we have buffered all available content.
*
* @private {number}
* @const
*/
shaka.media.Playhead.FUDGE_FACTOR_ = 0.1;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.PresentationTimeline');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.SegmentReference');
/**
* Creates a PresentationTimeline.
*
* @param {?number} presentationStartTime The wall-clock time, in seconds,
* when the presentation started or will start. Only required for live.
* @param {number} presentationDelay The delay to give the presentation, in
* seconds. Only required for live.
*
* @see {shakaExtern.Manifest}
*
* @constructor
* @struct
* @export
*/
shaka.media.PresentationTimeline = function(
presentationStartTime, presentationDelay) {
/** @private {?number} */
this.presentationStartTime_ = presentationStartTime;
/** @private {number} */
this.presentationDelay_ = presentationDelay;
/** @private {number} */
this.duration_ = Infinity;
/** @private {number} */
this.segmentAvailabilityDuration_ = Infinity;
/** @private {?number} */
this.maxSegmentDuration_ = 1;
/** @private {number} */
this.maxFirstSegmentStartTime_ = 0;
/** @private {number} */
this.clockOffset_ = 0;
/** @private {boolean} */
this.static_ = true;
};
/**
* @return {number} The presentation's duration in seconds.
* Infinity indicates that the presentation continues indefinitely.
* @export
*/
shaka.media.PresentationTimeline.prototype.getDuration = function() {
return this.duration_;
};
/**
* Sets the presentation's duration.
*
* @param {number} duration The presentation's duration in seconds.
* Infinity indicates that the presentation continues indefinitely.
* @export
*/
shaka.media.PresentationTimeline.prototype.setDuration = function(duration) {
goog.asserts.assert(duration > 0, 'duration must be > 0');
this.duration_ = duration;
};
/**
* Sets the clock offset, which is the the difference between the client's clock
* and the server's clock, in milliseconds (i.e., serverTime = Date.now() +
* clockOffset).
*
* @param {number} offset The clock offset, in ms.
* @export
*/
shaka.media.PresentationTimeline.prototype.setClockOffset = function(offset) {
this.clockOffset_ = offset;
};
/**
* Sets the presentation's static flag.
*
* @param {boolean} isStatic If true, the presentation is static, meaning all
* segments are available at once.
* @export
*/
shaka.media.PresentationTimeline.prototype.setStatic = function(isStatic) {
// NOTE: the argument name is not "static" because that's a keyword in ES6
this.static_ = isStatic;
};
/**
* Gets the presentation's segment availability duration, which is the amount
* of time, in seconds, that the start of a segment remains available after the
* live-edge moves past the end of that segment. Infinity indicates that
* segments remain available indefinitely. For example, if your live
* presentation has a 5 minute DVR window and your segments are 10 seconds long
* then the segment availability duration should be 4 minutes and 50 seconds.
*
* @return {number} The presentation's segment availability duration.
* @export
*/
shaka.media.PresentationTimeline.prototype.getSegmentAvailabilityDuration =
function() {
return this.segmentAvailabilityDuration_;
};
/**
* Sets the presentation's segment availability duration. The segment
* availability duration should only be set for live.
*
* @param {number} segmentAvailabilityDuration The presentation's new segment
* availability duration in seconds.
* @export
*/
shaka.media.PresentationTimeline.prototype.setSegmentAvailabilityDuration =
function(segmentAvailabilityDuration) {
goog.asserts.assert(segmentAvailabilityDuration >= 0,
'segmentAvailabilityDuration must be >= 0');
this.segmentAvailabilityDuration_ = segmentAvailabilityDuration;
};
/**
* Gives PresentationTimeline a Stream's segments so it can size and position
* the segment availability window, and account for missing segment
* information. This function should be called once for each Stream (no more,
* no less).
*
* @param {number} periodStartTime
* @param {!Array.<!shaka.media.SegmentReference>} references
* @export
*/
shaka.media.PresentationTimeline.prototype.notifySegments = function(
periodStartTime, references) {
if (references.length == 0)
return;
this.maxSegmentDuration_ = references.reduce(
function(max, r) { return Math.max(max, r.endTime - r.startTime); },
this.maxSegmentDuration_);
if (periodStartTime == 0) {
this.maxFirstSegmentStartTime_ =
Math.max(this.maxFirstSegmentStartTime_, references[0].startTime);
}
shaka.log.v1('notifySegments:',
'maxSegmentDuration=' + this.maxSegmentDuration_,
'maxFirstSegmentStartTime=' + this.maxFirstSegmentStartTime_);
};
/**
* Gives PresentationTimeline a Stream's maximum segment duration so it can
* size and position the segment availability window. This function should be
* called once for each Stream (no more, no less), but does not have to be
* called if notifySegments() is called instead for a particular stream.
*
* @param {number} maxSegmentDuration The maximum segment duration for a
* particular stream.
* @export
*/
shaka.media.PresentationTimeline.prototype.notifyMaxSegmentDuration = function(
maxSegmentDuration) {
this.maxSegmentDuration_ = Math.max(
this.maxSegmentDuration_, maxSegmentDuration);
shaka.log.v1('notifyNewSegmentDuration:',
'maxSegmentDuration=' + this.maxSegmentDuration_);
};
/**
* @return {boolean} True if the presentation is live; otherwise, return
* false.
* @export
*/
shaka.media.PresentationTimeline.prototype.isLive = function() {
return this.duration_ == Infinity &&
!this.static_;
};
/**
* @return {boolean} True if the presentation is in progress (meaning not live,
* but also not completely available); otherwise, return false.
* @export
*/
shaka.media.PresentationTimeline.prototype.isInProgress = function() {
return this.duration_ != Infinity &&
!this.static_;
};
/**
* Gets the presentation's current earliest, available timestamp. This value
* may be greater than the presentation's current segment availability start
* time if segment information is missing or does not exist at the beginning of
* the segment availability window.
*
* @return {number} The presentation's current earliest, available timestamp,
* in seconds, relative to the start of the presentation.
* @export
*/
shaka.media.PresentationTimeline.prototype.getEarliestStart = function() {
var maxFirstSegmentStartTime = Math.min(
this.maxFirstSegmentStartTime_, this.getSegmentAvailabilityEnd());
return Math.max(maxFirstSegmentStartTime,
this.getSegmentAvailabilityStart());
};
/**
* Gets the presentation's current segment availability start time. Segments
* ending at or before this time should be assumed to be unavailable.
*
* @return {number} The current segment availability start time, in seconds,
* relative to the start of the presentation.
* @export
*/
shaka.media.PresentationTimeline.prototype.getSegmentAvailabilityStart =
function() {
if (this.segmentAvailabilityDuration_ == Infinity)
return 0;
var start =
this.getSegmentAvailabilityEnd() - this.segmentAvailabilityDuration_;
return Math.max(0, start);
};
/**
* Gets the presentation's current segment availability end time. Segments
* starting after this time should be assumed to be unavailable.
*
* @return {number} The current segment availability end time, in seconds,
* relative to the start of the presentation. Always returns the
* presentation's duration for video-on-demand.
* @export
*/
shaka.media.PresentationTimeline.prototype.getSegmentAvailabilityEnd =
function() {
if (!this.isLive() && !this.isInProgress())
return this.duration_;
return Math.min(this.getLiveEdge_(), this.duration_);
};
/**
* Gets the seek range end.
*
* @return {number}
* @export
*/
shaka.media.PresentationTimeline.prototype.getSeekRangeEnd = function() {
var useDelay = this.isLive() || this.isInProgress();
var delay = useDelay ? this.presentationDelay_ : 0;
return Math.max(0, this.getSegmentAvailabilityEnd() - delay);
};
/**
* @return {number} The current presentation time in seconds.
* @private
*/
shaka.media.PresentationTimeline.prototype.getLiveEdge_ = function() {
goog.asserts.assert(this.presentationStartTime_ != null,
'Cannot compute timeline live edge without start time');
var now = (Date.now() + this.clockOffset_) / 1000.0;
return Math.max(
0, now - this.maxSegmentDuration_ - this.presentationStartTime_);
};
if (!COMPILED) {
/**
* Debug only: assert that the timeline parameters make sense for the type of
* presentation (VOD, IPR, live).
*/
shaka.media.PresentationTimeline.prototype.assertIsValid = function() {
if (this.isLive()) {
// Implied by isLive(): infinite and dynamic.
// Live streams should have a start time.
goog.asserts.assert(this.presentationStartTime_ != null,
'Detected as live stream, but does not match our model of live!');
} else if (this.isInProgress()) {
// Implied by isInProgress(): finite and dynamic.
// IPR streams should have a start time, and segments should not expire.
goog.asserts.assert(this.presentationStartTime_ != null &&
this.segmentAvailabilityDuration_ == Infinity,
'Detected as IPR stream, but does not match our model of IPR!');
} else { // VOD
// VOD segments should not expire and the presentation should be finite
// and static.
goog.asserts.assert(this.segmentAvailabilityDuration_ == Infinity &&
this.duration_ != Infinity &&
this.static_,
'Detected as VOD stream, but does not match our model of VOD!');
}
};
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.SegmentIndex');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.util.IDestroyable');
/**
* Creates a SegmentIndex.
*
* @param {!Array.<!shaka.media.SegmentReference>} references The list of
* SegmentReferences, which must be sorted first by their start times
* (ascending) and second by their end times (ascending), and have
* continuous, increasing positions.
*
* @constructor
* @struct
* @implements {shaka.util.IDestroyable}
* @export
*/
shaka.media.SegmentIndex = function(references) {
if (!COMPILED) {
shaka.media.SegmentIndex.assertCorrectReferences_(references);
}
/** @private {Array.<!shaka.media.SegmentReference>} */
this.references_ = references;
};
/**
* @override
* @export
*/
shaka.media.SegmentIndex.prototype.destroy = function() {
this.references_ = null;
return Promise.resolve();
};
/**
* Finds the position of the segment for the given time, in seconds, relative
* to the start of a particular Period. Returns the position of the segment
* with the largest end time if more than one segment is known for the given
* time.
*
* @param {number} time
* @return {?number} The position of the segment, or null
* if the position of the segment could not be determined.
* @export
*/
shaka.media.SegmentIndex.prototype.find = function(time) {
// For live streams, searching from the end is faster. For VOD, it balances
// out either way. In both cases, references_.length is small enough that the
// difference isn't huge.
for (var i = this.references_.length - 1; i >= 0; --i) {
var r = this.references_[i];
// Note that a segment ends immediately before the end time.
if ((time >= r.startTime) && (time < r.endTime)) {
return r.position;
}
}
return null;
};
/**
* Gets the SegmentReference for the segment at the given position.
*
* @param {number} position The position of the segment.
* @return {shaka.media.SegmentReference} The SegmentReference, or null if
* no such SegmentReference exists.
* @export
*/
shaka.media.SegmentIndex.prototype.get = function(position) {
if (this.references_.length == 0)
return null;
var index = position - this.references_[0].position;
if (index < 0 || index >= this.references_.length)
return null;
return this.references_[index];
};
/**
* Merges the given SegmentReferences. Supports extending the original
* references only. Will not replace old references or interleave new ones.
*
* @param {!Array.<!shaka.media.SegmentReference>} references The list of
* SegmentReferences, which must be sorted first by their start times
* (ascending) and second by their end times (ascending), and have
* continuous, increasing positions.
* @export
*/
shaka.media.SegmentIndex.prototype.merge = function(references) {
if (!COMPILED) {
shaka.media.SegmentIndex.assertCorrectReferences_(references);
}
var newReferences = [];
var i = 0;
var j = 0;
while ((i < this.references_.length) && (j < references.length)) {
var r1 = this.references_[i];
var r2 = references[j];
if (r1.startTime < r2.startTime) {
newReferences.push(r1);
i++;
} else if (r1.startTime > r2.startTime) {
// Drop the new reference if it would have to be interleaved with the
// old one. Issue a warning, since this is not a supported update.
shaka.log.warning('Refusing to rewrite original references on update!');
j++;
} else {
// When period is changed, fitSegmentReference will expand the last
// segment to the start of the next period. So, it is valid to have end
// time updated to the last segment reference in a period
if (Math.abs(r1.endTime - r2.endTime) > 0.1) {
goog.asserts.assert(r2.endTime > r1.endTime &&
i == this.references_.length - 1 &&
j == references.length - 1,
'This should be an update of the last segment in a period');
newReferences.push(r2);
} else {
// Drop the new reference if there's an old reference with the
// same time.
newReferences.push(r1);
}
i++;
j++;
}
}
while (i < this.references_.length) {
newReferences.push(this.references_[i++]);
}
if (newReferences.length) {
// The rest of these refs may need to be renumbered.
var nextPosition = newReferences[newReferences.length - 1].position + 1;
while (j < references.length) {
var r = references[j++];
var r2 = new shaka.media.SegmentReference(nextPosition++,
r.startTime, r.endTime, r.getUris, r.startByte, r.endByte);
newReferences.push(r2);
}
} else {
newReferences = references;
}
if (!COMPILED) {
shaka.media.SegmentIndex.assertCorrectReferences_(newReferences);
}
this.references_ = newReferences;
};
/**
* Removes all SegmentReferences that end before the given time.
*
* @param {number} time The time in seconds.
* @export
*/
shaka.media.SegmentIndex.prototype.evict = function(time) {
for (var i = 0; i < this.references_.length; ++i) {
if (this.references_[i].endTime > time)
break;
}
this.references_.splice(0, i);
};
if (!COMPILED) {
/**
* Asserts that the given SegmentReferences are sorted and have continuous,
* increasing positions.
*
* @param {!Array.<shaka.media.SegmentReference>} references
* @private
*/
shaka.media.SegmentIndex.assertCorrectReferences_ = function(references) {
goog.asserts.assert(references.every(function(r2, i) {
if (i == 0) return true;
var r1 = references[i - 1];
if (r2.position != r1.position + 1) return false;
if (r1.startTime < r2.startTime) {
return true;
} else if (r1.startTime > r2.startTime) {
return false;
} else {
if (r1.endTime <= r2.endTime) {
return true;
} else {
return false;
}
}
}), 'SegmentReferences are incorrect');
};
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.InitSegmentReference');
goog.provide('shaka.media.SegmentReference');
goog.require('goog.asserts');
/**
* Creates an InitSegmentReference, which provides the location to an
* initialization segment.
*
* @param {function():!Array.<string>} uris
* A function that creates the URIs of the resource containing the segment.
* @param {number} startByte The offset from the start of the resource to the
* start of the segment.
* @param {?number} endByte The offset from the start of the resource to the
* end of the segment, inclusive. null indicates that the segment extends
* to the end of the resource.
*
* @constructor
* @struct
* @export
*/
shaka.media.InitSegmentReference = function(uris, startByte, endByte) {
/** @type {function():!Array.<string>} */
this.getUris = uris;
/** @const {number} */
this.startByte = startByte;
/** @const {?number} */
this.endByte = endByte;
};
/**
* Creates a SegmentReference, which provides the start time, end time, and
* location to a media segment.
*
* @param {number} position The segment's position within a particular Period.
* The following should hold true between any two SegmentReferences from the
* same Period, r1 and r2:
* IF r2.position > r1.position THEN
* [ (r2.startTime > r1.startTime) OR
* (r2.startTime == r1.startTime AND r2.endTime >= r1.endTime) ]
* @param {number} startTime The segment's start time in seconds, relative to
* the start of a particular Period.
* @param {number} endTime The segment's end time in seconds, relative to
* the start of a particular Period. The segment ends the instant before
* this time, so |endTime| must be strictly greater than |startTime|.
* @param {function():!Array.<string>} uris
* A function that creates the URIs of the resource containing the segment.
* @param {number} startByte The offset from the start of the resource to the
* start of the segment.
* @param {?number} endByte The offset from the start of the resource to the
* end of the segment, inclusive. null indicates that the segment extends
* to the end of the resource.
*
* @constructor
* @struct
* @export
*/
shaka.media.SegmentReference = function(
position, startTime, endTime, uris, startByte, endByte) {
goog.asserts.assert(startTime < endTime,
'startTime must be less than endTime');
goog.asserts.assert((startByte < endByte) || (endByte == null),
'startByte must be < endByte');
/** @const {number} */
this.position = position;
/** @const {number} */
this.startTime = startTime;
/** @const {number} */
this.endTime = endTime;
/** @type {function():!Array.<string>} */
this.getUris = uris;
/** @const {number} */
this.startByte = startByte;
/** @const {?number} */
this.endByte = endByte;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.StreamingEngine');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.MediaSourceEngine');
goog.require('shaka.media.Playhead');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.Error');
goog.require('shaka.util.Functional');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.MapUtils');
goog.require('shaka.util.PublicPromise');
goog.require('shaka.util.StreamUtils');
/**
* Creates a StreamingEngine.
*
* The StreamingEngine is responsible for setting up the Manifest's Streams
* (i.e., for calling each Stream's createSegmentIndex() function), for
* downloading segments, for co-ordinating audio, video, and text buffering,
* and for handling Period transitions. The StreamingEngine provides an
* interface to switch between Streams, but it does not choose which Streams to
* switch to.
*
* The StreamingEngine notifies its owner when it needs to buffer a new Period,
* so its owner can choose which Streams within that Period to initially
* buffer. Moreover, the StreamingEngine also notifies its owner when any
* Stream within the current Period may be switched to, so its owner can switch
* bitrates, resolutions, or languages.
*
* The StreamingEngine does not need to be notified about changes to the
* Manifest's SegmentIndexes; however, it does need to be notified when new
* Periods are added to the Manifest, so it can set up that Period's Streams.
*
* To start the StreamingEngine the owner must first call configure() followed
* by init(). The StreamingEngine will then call onChooseStreams(p) when it
* needs to buffer Period p; it will then switch to the Streams returned from
* that function. The StreamingEngine will call onCanSwitch() when any
* Stream within the current Period may be switched to.
*
* The owner must call seeked() each time the playhead moves to a new location
* within the presentation timeline; however, the owner may forego calling
* seeked() when the playhead moves outside the presentation timeline.
*
* @param {!shaka.media.Playhead} playhead The Playhead. The caller retains
* ownership.
* @param {!shaka.media.MediaSourceEngine} mediaSourceEngine The
* MediaSourceEngine. The caller retains ownership.
* @param {shaka.net.NetworkingEngine} netEngine
* @param {shakaExtern.Manifest} manifest
* @param {function(!shakaExtern.Period): !Object.<string, shakaExtern.Stream>}
* onChooseStreams Called when the given Period needs to be buffered. The
* StreamingEngine will switch to the Streams returned from this function.
* The caller cannot call switch() directly until the StreamingEngine calls
* onCanSwitch()
* @param {function()} onCanSwitch Called when any Stream within the current
* Period may be switched to.
* @param {function(!shaka.util.Error)} onError Called when an error occurs.
* If the error is recoverable (see @link{shaka.util.Error}) then the
* caller may invoke either StreamingEngine.switch() or
* StreamingEngine.seeked() to attempt recovery.
* @param {function()=} opt_onInitialStreamsSetup Optional callback which
* is called when the initial set of Streams have been setup. Intended
* to be used by tests.
* @param {function()=} opt_onStartupComplete Optional callback which
* is called when startup has completed. Intended to be used by tests.
*
* @constructor
* @struct
* @implements {shaka.util.IDestroyable}
*/
shaka.media.StreamingEngine = function(
playhead, mediaSourceEngine, netEngine, manifest,
onChooseStreams, onCanSwitch, onError,
opt_onInitialStreamsSetup, opt_onStartupComplete) {
/** @private {shaka.media.Playhead} */
this.playhead_ = playhead;
/** @private {shaka.media.MediaSourceEngine} */
this.mediaSourceEngine_ = mediaSourceEngine;
/** @private {shaka.net.NetworkingEngine} */
this.netEngine_ = netEngine;
/** @private {?shakaExtern.Manifest} */
this.manifest_ = manifest;
/**
* @private
* {?function(!shakaExtern.Period): !Object.<string, shakaExtern.Stream>}
*/
this.onChooseStreams_ = onChooseStreams;
/** @private {?function()} */
this.onCanSwitch_ = onCanSwitch;
/** @private {?function(!shaka.util.Error)} */
this.onError_ = onError;
/** @private {?function()} */
this.onInitialStreamsSetup_ = opt_onInitialStreamsSetup || null;
/** @private {?function()} */
this.onStartupComplete_ = opt_onStartupComplete || null;
/** @private {?shakaExtern.StreamingConfiguration} */
this.config_ = null;
/** @private {number} */
this.bufferingGoalScale_ = 1;
/** @private {Promise} */
this.setupPeriodPromise_ = Promise.resolve();
/**
* Maps a Period's index to an object that indicates that either
* 1. the Period has not been set up (undefined)
* 2. the Period is being set up ([a PublicPromise, false]),
* 3. the Period is set up (i.e., all Streams within the Period are set up)
* and can be switched to ([a PublicPromise, true]).
*
* @private {Array.<?{promise: shaka.util.PublicPromise, resolved: boolean}>}
*/
this.canSwitchPeriod_ = [];
/**
* Maps a Stream's ID to an object that indicates that either
* 1. the Stream has not been set up (undefined)
* 2. the Stream is being set up ([a Promise instance, false]),
* 3. the Stream is set up and can be switched to
* ([a Promise instance, true]).
*
* @private {Object.<number,
* ?{promise: shaka.util.PublicPromise, resolved: boolean}>}
*/
this.canSwitchStream_ = {};
/**
* Maps a content type, e.g., 'audio', 'video', or 'text', to a MediaState.
*
* @private {Object.<string, !shaka.media.StreamingEngine.MediaState_>}
*/
this.mediaStates_ = {};
/**
* Set to true once one segment of each content type has been buffered.
*
* @private {boolean}
*/
this.startupComplete_ = false;
/**
* Set to true on fatal error. Interrupts fetchAndAppend_().
*
* @private {boolean}
*/
this.fatalError_ = false;
/** @private {boolean} */
this.destroyed_ = false;
};
/**
* @typedef {{
* type: string,
* stream: shakaExtern.Stream,
* lastStream: ?shakaExtern.Stream,
* lastSegmentReference: shaka.media.SegmentReference,
* needInitSegment: boolean,
* needPeriodIndex: number,
* endOfStream: boolean,
* performingUpdate: boolean,
* updateTimer: ?number,
* waitingToClearBuffer: boolean,
* waitingToFlushBuffer: boolean,
* clearingBuffer: boolean,
* recovering: boolean,
* hasError: boolean,
* resumeAt: number
* }}
*
* @description
* Contains the state of a logical stream, i.e., a sequence of segmented data
* for a particular content type. At any given time there is a Stream object
* associated with the state of the logical stream.
*
* @property {string} type
* The stream's content type, e.g., 'audio', 'video', or 'text'.
* @property {shakaExtern.Stream} stream
* The current Stream.
* @property {?shakaExtern.Stream} lastStream
* The Stream of the last segment that was appended.
* @property {shaka.media.SegmentReference} lastSegmentReference
* The SegmentReference of the last segment that was appended.
* @property {boolean} needInitSegment
* True indicates that |stream|'s init segment must be inserted before the
* next media segment is appended.
* @property {boolean} endOfStream
* True indicates that the end of the buffer has hit the end of the
* presentation.
* @property {number} needPeriodIndex
* The index of the Period which needs to be buffered.
* @property {boolean} performingUpdate
* True indicates that an update is in progress.
* @property {?number} updateTimer
* A non-null value indicates that an update is scheduled.
* @property {boolean} waitingToClearBuffer
* True indicates that the buffer must be cleared after the current update
* finishes.
* @property {boolean} waitingToFlushBuffer
* True indicates that the buffer must be flushed after it is cleared.
* @property {boolean} clearingBuffer
* True indicates that the buffer is being cleared.
* @property {boolean} recovering
* True indicates that the last segment was not appended because it could not
* fit in the buffer.
* @property {boolean} hasError
* True indicates that the stream has encountered an error and has stopped
* updates.
* @property {number} resumeAt
* An override for the time to start performing updates at. If the playhead
* is behind this time, update_() will still start fetching segments from
* this time. If the playhead is ahead of the time, this field is ignored.
*/
shaka.media.StreamingEngine.MediaState_;
/**
* The minimum number seconds that will remain buffered after evicting media.
*
* @const {number}
*/
shaka.media.StreamingEngine.prototype.MIN_BUFFER_LENGTH = 2;
/**
* Gets the StreamingEngine's rebuffering goal.
*
* @param {shakaExtern.Manifest} manifest
* @param {shakaExtern.StreamingConfiguration} config
* @param {number} scaleFactor
*
* @return {number}
*/
shaka.media.StreamingEngine.getRebufferingGoal = function(
manifest, config, scaleFactor) {
return scaleFactor *
Math.max(manifest.minBufferTime || 0, config.rebufferingGoal);
};
/** @override */
shaka.media.StreamingEngine.prototype.destroy = function() {
for (var type in this.mediaStates_) {
this.cancelUpdate_(this.mediaStates_[type]);
}
this.playhead_ = null;
this.mediaSourceEngine_ = null;
this.netEngine_ = null;
this.manifest_ = null;
this.setupPeriodPromise_ = null;
this.onChooseStreams_ = null;
this.onCanSwitch_ = null;
this.onError_ = null;
this.onInitialStreamsSetup_ = null;
this.onStartupComplete_ = null;
this.canSwitchPeriod_ = null;
this.canSwitchStream_ = null;
this.mediaStates_ = null;
this.config_ = null;
this.destroyed_ = true;
return Promise.resolve();
};
/**
* Called by the Player to provide an updated configuration any time it changes.
* Will be called at least once before init().
*
* @param {shakaExtern.StreamingConfiguration} config
*/
shaka.media.StreamingEngine.prototype.configure = function(config) {
this.config_ = config;
goog.asserts.assert(this.manifest_, 'manifest_ should not be null');
var rebufferingGoal = shaka.media.StreamingEngine.getRebufferingGoal(
this.manifest_, this.config_, this.bufferingGoalScale_);
this.playhead_.setRebufferingGoal(rebufferingGoal);
};
/**
* Initializes the StreamingEngine.
*
* After this function is called the StreamingEngine will call
* onChooseStreams(p) when it needs to buffer Period p and onCanSwitch() when
* any Stream within that Period may be switched to.
*
* After the StreamingEngine calls onChooseStreams(p) for the first time, it
* will begin setting up the Streams returned from that function and
* subsequently switch to them. However, the StreamingEngine will not begin
* setting up any other Streams until at least one segment from each of the
* initial set of Streams has been buffered (this reduces startup latency).
* After the StreamingEngine completes this startup phase it will begin setting
* up each Period's Streams (while buffering in parrallel).
*
* When the StreamingEngine needs to buffer the next Period it will have
* already set up that Period's Streams. So, when the StreamingEngine calls
* onChooseStreams(p) after the first time, the StreamingEngine will
* immediately switch to the Streams returned from that function.
*
* @return {!Promise}
*/
shaka.media.StreamingEngine.prototype.init = function() {
var MapUtils = shaka.util.MapUtils;
goog.asserts.assert(this.config_,
'StreamingEngine configure() must be called before init()!');
// Determine which Period we must buffer.
var playheadTime = this.playhead_.getTime();
var needPeriodIndex = this.findPeriodContainingTime_(playheadTime);
// Get the initial set of Streams.
var streamsByType =
this.onChooseStreams_(this.manifest_.periods[needPeriodIndex]);
if (MapUtils.empty(streamsByType)) {
shaka.log.error('init: no Streams chosen');
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.STREAMING,
shaka.util.Error.Code.INVALID_STREAMS_CHOSEN));
}
// Setup the initial set of Streams and then begin each update cycle. After
// startup completes onUpdate_() will set up the remaining Periods.
return this.initStreams_(streamsByType).then(function() {
shaka.log.debug('init: completed initial Stream setup');
// Subtlety: onInitialStreamsSetup_() may call switch() or seeked(), so we
// must schedule an update beforehand so |updateTimer| is set.
if (this.onInitialStreamsSetup_) {
shaka.log.v1('init: calling onInitialStreamsSetup_()...');
this.onInitialStreamsSetup_();
}
}.bind(this));
};
/**
* Gets the current Period the stream is in. This Period may not be initialized
* yet if canSwitch(period) has not been called yet.
* @return {shakaExtern.Period}
*/
shaka.media.StreamingEngine.prototype.getCurrentPeriod = function() {
var playheadTime = this.playhead_.getTime();
var needPeriodIndex = this.findPeriodContainingTime_(playheadTime);
return this.manifest_.periods[needPeriodIndex];
};
/**
* Gets a map of all the active streams.
* @return {!Object.<string, shakaExtern.Stream>}
*/
shaka.media.StreamingEngine.prototype.getActiveStreams = function() {
goog.asserts.assert(this.mediaStates_, 'Must be initialized');
var MapUtils = shaka.util.MapUtils;
return MapUtils.map(
this.mediaStates_, function(state) { return state.stream; });
};
/**
* Notifies StreamingEngine that a new stream was added to the manifest. This
* initializes the given stream. This returns a Promise that resolves when
* the stream has been set up.
*
* @param {string} type
* @param {shakaExtern.Stream} stream
* @return {!Promise}
*/
shaka.media.StreamingEngine.prototype.notifyNewStream = function(type, stream) {
/** @type {!Object.<string, shakaExtern.Stream>} */
var streamsByType = {};
streamsByType[type] = stream;
return this.initStreams_(streamsByType);
};
/**
* Switches to the given Stream. |stream| may be from any StreamSet or any
* Period.
*
* @param {string} contentType |stream|'s content type.
* @param {shakaExtern.Stream} stream
* @param {boolean} clearBuffer
*/
shaka.media.StreamingEngine.prototype.switch = function(
contentType, stream, clearBuffer) {
var mediaState = this.mediaStates_[contentType];
if (!mediaState && contentType == 'text' &&
this.config_.ignoreTextStreamFailures) {
this.notifyNewStream('text', stream);
return;
}
goog.asserts.assert(mediaState, 'switch: expected mediaState to exist');
if (!mediaState) return;
// Ensure the Period is ready.
var periodIndex = this.findPeriodContainingStream_(stream);
var canSwitchRecord = this.canSwitchPeriod_[periodIndex];
goog.asserts.assert(
canSwitchRecord && canSwitchRecord.resolved,
'switch: expected Period ' + periodIndex + ' to be ready');
if (!canSwitchRecord || !canSwitchRecord.resolved) return;
// Sanity check. If the Period is ready then the Stream should be ready too.
canSwitchRecord = this.canSwitchStream_[stream.id];
goog.asserts.assert(canSwitchRecord && canSwitchRecord.resolved,
'switch: expected Stream ' + stream.id + ' to be ready');
if (!canSwitchRecord || !canSwitchRecord.resolved) return;
if (mediaState.stream == stream) {
var streamTag = shaka.media.StreamingEngine.logPrefix_(mediaState);
shaka.log.debug('switch: Stream ' + streamTag + ' already active');
return;
}
mediaState.stream = stream;
mediaState.needInitSegment = true;
var streamTag = shaka.media.StreamingEngine.logPrefix_(mediaState);
shaka.log.debug('switch: switching to Stream ' + streamTag);
if (clearBuffer) {
if (mediaState.clearingBuffer) {
// We are already going to clear the buffer, but make sure it is also
// flushed.
mediaState.waitingToFlushBuffer = true;
} else if (mediaState.performingUpdate) {
// We are performing an update, so we have to wait until it's finished.
// onUpdate_() will call clearBuffer_() when the update has
// finished.
mediaState.waitingToClearBuffer = true;
mediaState.waitingToFlushBuffer = true;
} else {
// Cancel the update timer, if any.
this.cancelUpdate_(mediaState);
// Clear right away.
this.clearBuffer_(mediaState, /* flush */ true);
}
}
};
/**
* Notifies the StreamingEngine that the playhead has moved to a valid time
* within the presentation timeline.
*/
shaka.media.StreamingEngine.prototype.seeked = function() {
goog.asserts.assert(this.mediaStates_, 'Must not be destroyed');
var playheadTime = this.playhead_.getTime();
var isAllBuffered = Object.keys(this.mediaStates_).every(function(type) {
// Don't use a fudge factor here since Chrome doesn't jump gaps after a seek
// https://github.com/google/shaka-player/issues/655
return this.mediaSourceEngine_.bufferedAheadOf(type, playheadTime) > 0;
}.bind(this));
// Only treat as a buffered seek if every media state has a buffer. For
// example, if we have buffered text but not video, we should still clear
// every buffer so all media states need the same Period.
if (isAllBuffered) {
shaka.log.debug(
'(all): seeked: buffered seek: playheadTime=' + playheadTime);
return;
}
// This was an unbuffered seek (for at least one stream), clear all buffers.
// Don't clear only some of the buffers because we can become stalled since
// the media states are waiting for different Periods.
for (var type in this.mediaStates_) {
var mediaState = this.mediaStates_[type];
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
if (mediaState.clearingBuffer) {
// We're already clearing the buffer, so we don't need to clear the
// buffer again.
shaka.log.debug(logPrefix, 'seeked: already clearing the buffer');
continue;
}
if (mediaState.waitingToClearBuffer) {
// May not be performing an update, but an update will still happen.
// See: https://github.com/google/shaka-player/issues/334
shaka.log.debug(logPrefix, 'seeked: unbuffered seek: already waiting');
continue;
}
if (mediaState.performingUpdate) {
// We are performing an update, so we have to wait until it's finished.
// onUpdate_() will call clearBuffer_() when the update has
// finished.
shaka.log.debug(logPrefix, 'seeked: unbuffered seek: currently updating');
mediaState.waitingToClearBuffer = true;
continue;
}
if (this.mediaSourceEngine_.bufferStart(type) == null) {
// Nothing buffered.
shaka.log.debug(logPrefix, 'seeked: unbuffered seek: nothing buffered');
if (mediaState.updateTimer == null) {
// Note: an update cycle stops when we buffer to the end of the
// presentation or Period, or when we raise an error.
this.scheduleUpdate_(mediaState, 0);
}
continue;
}
// An update may be scheduled, but we can just cancel it and clear the
// buffer right away. Note: clearBuffer_() will schedule the next update.
shaka.log.debug(logPrefix, 'seeked: unbuffered seek: handling right now');
this.cancelUpdate_(mediaState);
this.clearBuffer_(mediaState, /* flush */ false);
}
};
/**
* Initializes the given streams and media states if required. This will
* schedule updates for the given types.
*
* @param {!Object.<string, shakaExtern.Stream>} streamsByType
* @param {number=} opt_resumeAt
* @return {!Promise}
* @private
*/
shaka.media.StreamingEngine.prototype.initStreams_ = function(
streamsByType, opt_resumeAt) {
var MapUtils = shaka.util.MapUtils;
goog.asserts.assert(this.config_,
'StreamingEngine configure() must be called before init()!');
// Determine which Period we must buffer.
var playheadTime = this.playhead_.getTime();
var needPeriodIndex = this.findPeriodContainingTime_(playheadTime);
// Init MediaSourceEngine.
var typeConfig = MapUtils.map(streamsByType, function(stream) {
return shaka.util.StreamUtils.getFullMimeType(
stream.mimeType, stream.codecs);
});
this.mediaSourceEngine_.init(typeConfig,
this.config_.useRelativeCueTimestamps);
this.setDuration_();
// Setup the initial set of Streams and then begin each update cycle. After
// startup completes onUpdate_() will set up the remaining Periods.
var streams = MapUtils.values(streamsByType);
return this.setupStreams_(streams).then(function() {
if (this.destroyed_) return;
for (var type in streamsByType) {
var stream = streamsByType[type];
if (!this.mediaStates_[type]) {
this.mediaStates_[type] = {
stream: stream,
type: type,
lastStream: null,
lastSegmentReference: null,
needInitSegment: true,
needPeriodIndex: needPeriodIndex,
endOfStream: false,
performingUpdate: false,
updateTimer: null,
waitingToClearBuffer: false,
waitingToFlushBuffer: false,
clearingBuffer: false,
recovering: false,
hasError: false,
resumeAt: opt_resumeAt || 0
};
this.scheduleUpdate_(this.mediaStates_[type], 0);
}
}
}.bind(this));
};
/**
* Sets up the given Period if necessary. Calls onError_() if an error
* occurs.
*
* @param {number} periodIndex The Period's index.
* @return {!Promise} A Promise which is resolved when the given Period is
* setup.
* @private
*/
shaka.media.StreamingEngine.prototype.setupPeriod_ = function(periodIndex) {
var Functional = shaka.util.Functional;
var canSwitchRecord = this.canSwitchPeriod_[periodIndex];
if (canSwitchRecord) {
shaka.log.debug(
'(all) Period ' + periodIndex + ' is being or has been set up');
goog.asserts.assert(canSwitchRecord.promise, 'promise must not be null');
return canSwitchRecord.promise;
}
shaka.log.debug('(all) setting up Period ' + periodIndex);
canSwitchRecord = {
promise: new shaka.util.PublicPromise(),
resolved: false
};
this.canSwitchPeriod_[periodIndex] = canSwitchRecord;
var streams = this.manifest_.periods[periodIndex].streamSets
.map(function(streamSet) { return streamSet.streams; })
.reduce(Functional.collapseArrays, []);
// Serialize Period set up.
this.setupPeriodPromise_ = this.setupPeriodPromise_.then(function() {
if (this.destroyed_) return;
return this.setupStreams_(streams);
}.bind(this)).then(function() {
if (this.destroyed_) return;
this.canSwitchPeriod_[periodIndex].promise.resolve();
this.canSwitchPeriod_[periodIndex].resolved = true;
shaka.log.v1('(all) setup Period ' + periodIndex);
}.bind(this)).catch(function(error) {
if (this.destroyed_) return;
this.canSwitchPeriod_[periodIndex].promise.reject();
delete this.canSwitchPeriod_[periodIndex];
shaka.log.warning('(all) failed to setup Period ' + periodIndex);
this.onError_(error);
// Don't stop other Periods from being set up.
}.bind(this));
return canSwitchRecord.promise;
};
/**
* Sets up the given Streams if necessary. Does NOT call onError_() if an
* error occurs.
*
* @param {!Array.<!shakaExtern.Stream>} streams
* @return {!Promise}
* @private
*/
shaka.media.StreamingEngine.prototype.setupStreams_ = function(streams) {
// Parallelize Stream set up.
var async = [];
for (var i = 0; i < streams.length; ++i) {
var stream = streams[i];
var canSwitchRecord = this.canSwitchStream_[stream.id];
if (canSwitchRecord) {
shaka.log.debug(
'(all) Stream ' + stream.id + ' is being or has been set up');
async.push(canSwitchRecord.promise);
} else {
shaka.log.v1('(all) setting up Stream ' + stream.id);
this.canSwitchStream_[stream.id] = {
promise: new shaka.util.PublicPromise(),
resolved: false
};
async.push(stream.createSegmentIndex());
}
}
return Promise.all(async).then(function() {
if (this.destroyed_) return;
for (var i = 0; i < streams.length; ++i) {
var stream = streams[i];
var canSwitchRecord = this.canSwitchStream_[stream.id];
if (!canSwitchRecord.resolved) {
canSwitchRecord.promise.resolve();
canSwitchRecord.resolved = true;
shaka.log.v1('(all) setup Stream ' + stream.id);
}
}
}.bind(this)).catch(function(error) {
if (this.destroyed_) return;
this.canSwitchStream_[stream.id].promise.reject();
delete this.canSwitchStream_[stream.id];
return Promise.reject(error);
}.bind(this));
};
/**
* Sets the MediaSource's duration.
* @private
*/
shaka.media.StreamingEngine.prototype.setDuration_ = function() {
var duration = this.manifest_.presentationTimeline.getDuration();
if (duration < Infinity) {
this.mediaSourceEngine_.setDuration(duration);
} else {
// Not all platforms support infinite durations, so set a finite duration
// so we can append segments and so the user agent can seek.
this.mediaSourceEngine_.setDuration(Math.pow(2, 32));
}
};
/**
* Called when |mediaState|'s update timer has expired.
*
* @param {!shaka.media.StreamingEngine.MediaState_} mediaState
* @private
*/
shaka.media.StreamingEngine.prototype.onUpdate_ = function(mediaState) {
var MapUtils = shaka.util.MapUtils;
if (this.destroyed_) return;
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
// Sanity check.
goog.asserts.assert(
!mediaState.performingUpdate && (mediaState.updateTimer != null),
logPrefix + ' unexpected call to onUpdate_()');
if (mediaState.performingUpdate || (mediaState.updateTimer == null)) return;
goog.asserts.assert(
!mediaState.clearingBuffer,
logPrefix + ' onUpdate_() should not be called when clearing the buffer');
if (mediaState.clearingBuffer) return;
mediaState.updateTimer = null;
// Handle pending buffer clears.
if (mediaState.waitingToClearBuffer) {
// Note: clearBuffer_() will schedule the next update.
shaka.log.debug(logPrefix, 'skipping update and clearing the buffer');
this.clearBuffer_(mediaState, mediaState.waitingToFlushBuffer);
return;
}
// Update the MediaState.
try {
var delay = this.update_(mediaState);
if (delay != null) {
this.scheduleUpdate_(mediaState, delay);
mediaState.hasError = false;
}
} catch (error) {
this.onError_(error);
return;
}
goog.asserts.assert(this.mediaStates_, 'must not be destroyed');
var mediaStates = MapUtils.values(this.mediaStates_);
// Check if we've buffered to the end of the Period.
this.handlePeriodTransition_(mediaState);
// Check if we've buffered to the end of the presentation.
if (mediaStates.every(function(ms) { return ms.endOfStream; })) {
shaka.log.v1(logPrefix, 'calling endOfStream()...');
this.mediaSourceEngine_.endOfStream();
}
};
/**
* Updates the given MediaState.
*
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @return {?number} The number of seconds to wait until updating again or
* null if another update does not need to be scheduled.
* @throws {!shaka.util.Error} if an error occurs.
* @private
*/
shaka.media.StreamingEngine.prototype.update_ = function(mediaState) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
// Compute how far we've buffered ahead of the playhead.
var playheadTime = this.playhead_.getTime();
// Get the next timestamp we need.
// TODO: see if we can refactor this logic to be less cumbersome
var bufferEnd = this.mediaSourceEngine_.bufferEnd(mediaState.type);
var timeNeeded = this.getTimeNeeded_(mediaState, playheadTime);
shaka.log.v2(logPrefix, 'timeNeeded=' + timeNeeded);
mediaState.resumeAt = 0;
var currentPeriodIndex = this.findPeriodContainingStream_(mediaState.stream);
var needPeriodIndex = this.findPeriodContainingTime_(timeNeeded);
// Get the amount of content we have buffered, accounting for drift. This
// is only used to determine if we have meet the buffering goal. This should
// be the same way that Playhead uses.
var bufferedAhead = this.mediaSourceEngine_.bufferedAheadOf(
mediaState.type, playheadTime, 0.1);
shaka.log.v2(logPrefix,
'update_:',
'playheadTime=' + playheadTime,
'bufferedAhead=' + bufferedAhead);
var bufferingGoal = this.getBufferingGoal_();
// Check if we've buffered to the end of the presentation.
if (timeNeeded >= this.manifest_.presentationTimeline.getDuration()) {
// We shouldn't rebuffer if the playhead is close to the end of the
// presentation.
shaka.log.debug(logPrefix, 'buffered to end of presentation');
mediaState.endOfStream = true;
return null;
}
mediaState.endOfStream = false;
// Check if we've buffered to the end of the Period. This should be done
// before checking segment availability because the new Period may become
// available once it's switched to. Note that we don't use the non-existence
// of SegmentReferences as an indicator to determine Period boundaries
// because SegmentIndexes can provide SegmentReferences outside its Period.
mediaState.needPeriodIndex = needPeriodIndex;
if (needPeriodIndex != currentPeriodIndex) {
shaka.log.debug(logPrefix,
'need Period ' + needPeriodIndex,
'playheadTime=' + playheadTime,
'timeNeeded=' + timeNeeded,
'currentPeriodIndex=' + currentPeriodIndex);
return null;
}
// If we've buffered to the buffering goal then schedule an update.
if (bufferedAhead >= bufferingGoal) {
shaka.log.v2(logPrefix, 'buffering goal met');
// Do not try to predict the next update. Just poll twice every second.
// The playback rate can change at any time, so any prediction we make now
// could be terribly invalid soon.
return 0.5;
}
var reference = this.getSegmentReferenceNeeded_(
mediaState, playheadTime, bufferEnd, currentPeriodIndex);
if (!reference) {
// The segment could not be found, does not exist, or is not available. In
// any case just try again... if the manifest is incomplete or is not being
// updated then we'll idle forever; otherwise, we'll end up getting a
// SegmentReference eventually.
return 1;
}
this.fetchAndAppend_(mediaState, playheadTime, currentPeriodIndex, reference);
return null;
};
/**
* Computes buffering goal.
*
* @return {number}
* @private
*/
shaka.media.StreamingEngine.prototype.getBufferingGoal_ = function() {
goog.asserts.assert(this.manifest_, 'manifest_ should not be null');
goog.asserts.assert(this.config_, 'config_ should not be null');
var rebufferingGoal = shaka.media.StreamingEngine.getRebufferingGoal(
this.manifest_, this.config_, this.bufferingGoalScale_);
return Math.max(
rebufferingGoal,
this.bufferingGoalScale_ * this.config_.bufferingGoal);
};
/**
* Gets the next timestamp needed. Returns the playhead's position if the
* buffer is empty; otherwise, returns the time at which the last segment
* appended ends.
*
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @param {number} playheadTime
* @return {number} The next timestamp needed.
* @throws {!shaka.util.Error} if the buffer is inconsistent with our
* expectations.
* @private
*/
shaka.media.StreamingEngine.prototype.getTimeNeeded_ = function(
mediaState, playheadTime) {
// Get the next timestamp we need. We must use |lastSegmentReference|
// to determine this and not the actual buffer for two reasons:
// 1. actual segments end slightly before their advertised end times, so
// the next timestamp we need is actually larger than |bufferEnd|; and
// 2. there may be drift (the timestamps in the segments are ahead/behind
// of the timestamps in the manifest), but we need drift free times when
// comparing times against presentation and Period boundaries.
if (!mediaState.lastStream || !mediaState.lastSegmentReference) {
return Math.max(playheadTime, mediaState.resumeAt);
}
var lastPeriodIndex =
this.findPeriodContainingStream_(mediaState.lastStream);
var lastPeriod = this.manifest_.periods[lastPeriodIndex];
return lastPeriod.startTime + mediaState.lastSegmentReference.endTime;
};
/**
* Gets the SegmentReference of the next segment needed.
*
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @param {number} playheadTime
* @param {?number} bufferEnd
* @param {number} currentPeriodIndex
* @return {shaka.media.SegmentReference} The SegmentReference of the
* next segment needed, or null if a segment could not be found, does not
* exist, or is not available.
* @private
*/
shaka.media.StreamingEngine.prototype.getSegmentReferenceNeeded_ = function(
mediaState, playheadTime, bufferEnd, currentPeriodIndex) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
if (mediaState.lastSegmentReference &&
mediaState.stream == mediaState.lastStream) {
// Something is buffered from the same Stream.
var position = mediaState.lastSegmentReference.position + 1;
shaka.log.v2(logPrefix, 'next position known:', 'position=' + position);
return this.getSegmentReferenceIfAvailable_(
mediaState, currentPeriodIndex, position);
}
var position;
if (mediaState.lastSegmentReference) {
// Something is buffered from another Stream.
goog.asserts.assert(mediaState.lastStream, 'lastStream should not be null');
shaka.log.v1(logPrefix, 'next position unknown: another Stream buffered');
var lastPeriodIndex =
this.findPeriodContainingStream_(mediaState.lastStream);
var lastPeriod = this.manifest_.periods[lastPeriodIndex];
position = this.lookupSegmentPosition_(
mediaState,
lastPeriod.startTime + mediaState.lastSegmentReference.endTime,
currentPeriodIndex);
} else {
// Either nothing is buffered, or we have cleared part of the buffer. If
// we still have some buffered, use that time to find the segment, otherwise
// start at the playhead time.
goog.asserts.assert(!mediaState.lastStream, 'lastStream should be null');
shaka.log.v1(logPrefix, 'next position unknown: nothing buffered');
position = this.lookupSegmentPosition_(
mediaState, bufferEnd || playheadTime, currentPeriodIndex);
}
if (position == null)
return null;
var reference = null;
if (bufferEnd == null) {
// If there's positive drift then we need to get the previous segment;
// however, we don't actually know how much drift there is, so we must
// unconditionally get the previous segment. If it turns out that there's
// non-positive drift then we'll just end up buffering beind the playhead a
// little more than we needed.
var optimalPosition = Math.max(0, position - 1);
reference = this.getSegmentReferenceIfAvailable_(
mediaState, currentPeriodIndex, optimalPosition);
}
return reference ||
this.getSegmentReferenceIfAvailable_(
mediaState, currentPeriodIndex, position);
};
/**
* Looks up the position of the segment containing the given timestamp.
*
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @param {number} presentationTime The timestamp needed, relative to the
* start of the presentation.
* @param {number} currentPeriodIndex
* @return {?number} A segment position, or null if a segment was not be found.
* @private
*/
shaka.media.StreamingEngine.prototype.lookupSegmentPosition_ = function(
mediaState, presentationTime, currentPeriodIndex) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
var currentPeriod = this.manifest_.periods[currentPeriodIndex];
shaka.log.debug(logPrefix,
'looking up segment:',
'presentationTime=' + presentationTime,
'currentPeriod.startTime=' + currentPeriod.startTime);
var lookupTime = Math.max(0, presentationTime - currentPeriod.startTime);
var position = mediaState.stream.findSegmentPosition(lookupTime);
if (position == null) {
shaka.log.warning(logPrefix,
'cannot find segment:',
'currentPeriod.startTime=' + currentPeriod.startTime,
'lookupTime=' + lookupTime);
}
return position;
};
/**
* Gets the SegmentReference at the given position if it's available.
*
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @param {number} currentPeriodIndex
* @param {number} position
* @return {shaka.media.SegmentReference}
*
* @private
*/
shaka.media.StreamingEngine.prototype.getSegmentReferenceIfAvailable_ =
function(mediaState, currentPeriodIndex, position) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
var currentPeriod = this.manifest_.periods[currentPeriodIndex];
var reference = mediaState.stream.getSegmentReference(position);
if (!reference) {
shaka.log.v1(logPrefix,
'segment does not exist:',
'currentPeriod.startTime=' + currentPeriod.startTime,
'position=' + position);
return null;
}
var timeline = this.manifest_.presentationTimeline;
var availabilityStart = timeline.getSegmentAvailabilityStart();
var availabilityEnd = timeline.getSegmentAvailabilityEnd();
if ((currentPeriod.startTime + reference.endTime < availabilityStart) ||
(currentPeriod.startTime + reference.startTime > availabilityEnd)) {
shaka.log.v2(logPrefix,
'segment is not available:',
'currentPeriod.startTime=' + currentPeriod.startTime,
'reference.startTime=' + reference.startTime,
'reference.endTime=' + reference.endTime,
'availabilityStart=' + availabilityStart,
'availabilityEnd=' + availabilityEnd);
return null;
}
return reference;
};
/**
* Fetches and appends the given segment; sets up the given MediaState's
* associated SourceBuffer and evicts segments if either are required
* beforehand. Schedules another update after completing successfully.
*
* @param {!shaka.media.StreamingEngine.MediaState_} mediaState
* @param {number} playheadTime
* @param {number} currentPeriodIndex The index of the current Period.
* @param {!shaka.media.SegmentReference} reference
* @private
*/
shaka.media.StreamingEngine.prototype.fetchAndAppend_ = function(
mediaState, playheadTime, currentPeriodIndex, reference) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
var currentPeriod = this.manifest_.periods[currentPeriodIndex];
shaka.log.v1(logPrefix,
'fetchAndAppend_:',
'playheadTime=' + playheadTime,
'currentPeriod.startTime=' + currentPeriod.startTime,
'reference.position=' + reference.position,
'reference.startTime=' + reference.startTime,
'reference.endTime=' + reference.endTime);
// Subtlety: The playhead may move while asynchronous update operations are
// in progress, so we should avoid calling playhead_.getTime() in any
// callbacks. Furthermore, switch() may be called at any time, so we should
// also avoid using mediaState.stream or mediaState.needInitSegment in any
// callbacks too.
var stream = mediaState.stream;
// Compute the append window end.
var followingPeriod = this.manifest_.periods[currentPeriodIndex + 1];
var appendWindowEnd = null;
if (followingPeriod) {
appendWindowEnd = followingPeriod.startTime;
} else {
appendWindowEnd = this.manifest_.presentationTimeline.getDuration();
}
goog.asserts.assert(
(appendWindowEnd == null) || (reference.startTime <= appendWindowEnd),
logPrefix + ' segment should start before append window end');
var initSourceBuffer =
this.initSourceBuffer_(mediaState, currentPeriodIndex, appendWindowEnd);
mediaState.performingUpdate = true;
// We may set |needInitSegment| to true in switch(), so set it to false here,
// since we want it to remain true if switch() is called.
mediaState.needInitSegment = false;
shaka.log.v2(logPrefix, 'fetching segment');
var fetchSegment = this.fetch_(reference);
Promise.all([initSourceBuffer, fetchSegment]).then(function(results) {
if (this.destroyed_ || this.fatalError_) return;
return this.append_(mediaState,
playheadTime,
currentPeriod,
stream,
reference,
results[1]);
}.bind(this)).then(function() {
if (this.destroyed_ || this.fatalError_) return;
mediaState.performingUpdate = false;
mediaState.recovering = false;
// Update right away.
this.scheduleUpdate_(mediaState, 0);
// Subtlety: handleStartup_() calls onStartupComplete_() which may call
// switch() or seeked(), so we must schedule an update beforehand so
// |updateTimer| is set.
this.handleStartup_(mediaState, stream);
shaka.log.v1(logPrefix, 'finished fetch and append');
}.bind(this)).catch(function(error) {
if (this.destroyed_ || this.fatalError_) return;
mediaState.performingUpdate = false;
if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS ||
error.code == shaka.util.Error.Code.HTTP_ERROR ||
error.code == shaka.util.Error.Code.TIMEOUT) {
this.handleNetworkError_(mediaState, error);
} else if (error.code == shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR) {
this.handleQuotaExceeded_(mediaState, error);
} else {
shaka.log.error(logPrefix, 'failed fetch and append: code=' + error.code);
if (mediaState.type == 'text' && this.config_.ignoreTextStreamFailures) {
shaka.log.warning(logPrefix,
'Text stream failed to parse. Proceeding without it.');
delete this.mediaStates_['text'];
} else {
mediaState.hasError = true;
this.onError_(error);
}
}
}.bind(this));
};
/**
* Handles a network error.
*
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @param {!shaka.util.Error} error
* @private
*/
shaka.media.StreamingEngine.prototype.handleNetworkError_ = function(
mediaState, error) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
if (mediaState.type == 'text' && this.config_.ignoreTextStreamFailures &&
error.code == shaka.util.Error.Code.BAD_HTTP_STATUS) {
shaka.log.warning(logPrefix,
'Text stream failed to download. Proceeding without it.');
delete this.mediaStates_['text'];
} else {
this.onError_(error);
shaka.log.warning(logPrefix, 'Network error. Retrying...');
this.scheduleUpdate_(mediaState, 4);
}
};
/**
* Handles a QUOTA_EXCEEDED_ERROR.
*
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @param {!shaka.util.Error} error
* @private
*/
shaka.media.StreamingEngine.prototype.handleQuotaExceeded_ = function(
mediaState, error) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
// The segment cannot fit into the SourceBuffer. Ideally, MediaSource would
// have evicted old data to accommodate the segment; however, it may have
// failed to do this if the segment is very large, or if it could not find
// a suitable time range to remove.
//
// We can overcome the latter by trying to append the segment again;
// however, to avoid continuous QuotaExceededErrors we must reduce the size
// of the buffer going forward.
//
// If we've recently reduced the buffering goals, wait until the stream
// which caused the first QuotaExceededError recovers. Doing this ensures
// we don't reduce the buffering goals too quickly.
goog.asserts.assert(this.mediaStates_, 'must not be destroyed');
var mediaStates = shaka.util.MapUtils.values(this.mediaStates_);
var waitingForAnotherStreamToRecover = mediaStates.some(function(ms) {
return ms != mediaState && ms.recovering;
});
if (!waitingForAnotherStreamToRecover) {
// Reduction schedule: 80%, 60%, 40%, 20%, 16%, 12%, 8%, 4%, fail.
// Note: percentages are used for comparisons to avoid rounding errors.
var percentBefore = Math.round(100 * this.bufferingGoalScale_);
if (percentBefore > 20) {
this.bufferingGoalScale_ -= 0.2;
} else if (percentBefore > 4) {
this.bufferingGoalScale_ -= 0.04;
} else {
shaka.log.error(
logPrefix, 'MediaSource threw QuotaExceededError too many times');
mediaState.hasError = true;
this.fatalError_ = true;
this.onError_(error);
return;
}
var percentAfter = Math.round(100 * this.bufferingGoalScale_);
shaka.log.warning(
logPrefix,
'MediaSource threw QuotaExceededError:',
'reducing buffering goals by ' + (100 - percentAfter) + '%');
mediaState.recovering = true;
} else {
shaka.log.debug(
logPrefix,
'MediaSource threw QuotaExceededError:',
'waiting for another stream to recover...');
}
// QuotaExceededError gets thrown if evication didn't help to make room
// for a segment. We want to wait for a while (4 seconds is just an
// arbitrary number) before updating to give the playhead a chance to
// advance, so we don't immidiately throw again.
this.scheduleUpdate_(mediaState, 4);
};
/**
* Sets the given MediaState's associated SourceBuffer's timestamp offset and
* init segment if either are required. If an error occurs then neither the
* timestamp offset or init segment are unset, since another call to switch()
* will end up superseding them.
*
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @param {number} currentPeriodIndex
* @param {?number} appendWindowEnd
* @return {!Promise}
* @private
*/
shaka.media.StreamingEngine.prototype.initSourceBuffer_ = function(
mediaState, currentPeriodIndex, appendWindowEnd) {
if (!mediaState.needInitSegment)
return Promise.resolve();
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
var currentPeriod = this.manifest_.periods[currentPeriodIndex];
// If we need an init segment then the Stream switched, so we've either
// changed bitrates, Periods, or both. If we've changed Periods then we must
// set a new timestamp offset and append window end. Note that by setting
// these values here, we avoid having to co-ordinate ongoing updates, which
// we would have to do if we instead set them in switch().
var timestampOffset =
currentPeriod.startTime - mediaState.stream.presentationTimeOffset;
shaka.log.v1(logPrefix, 'setting timestamp offset to ' + timestampOffset);
var setTimestampOffset = this.mediaSourceEngine_.setTimestampOffset(
mediaState.type, timestampOffset);
if (appendWindowEnd != null) {
shaka.log.v1(logPrefix, 'setting append window end to ' + appendWindowEnd);
var setAppendWindowEnd = this.mediaSourceEngine_.setAppendWindowEnd(
mediaState.type, appendWindowEnd);
} else {
setAppendWindowEnd = Promise.resolve();
}
if (!mediaState.stream.initSegmentReference) {
// The Stream is self initializing.
return Promise.all([setTimestampOffset, setAppendWindowEnd]);
}
shaka.log.v1(logPrefix, 'fetching init segment');
var fetchInit = this.fetch_(mediaState.stream.initSegmentReference);
var appendInit = fetchInit.then(function(initSegment) {
if (this.destroyed_) return;
shaka.log.v1(logPrefix, 'appending init segment');
return this.mediaSourceEngine_.appendBuffer(
mediaState.type, initSegment, null /* startTime */, null /* endTime */);
}.bind(this)).catch(function(error) {
mediaState.needInitSegment = true;
return Promise.reject(error);
});
return Promise.all([setTimestampOffset, setAppendWindowEnd, appendInit]);
};
/**
* Appends the given segment and evicts content if required to append.
*
* @param {!shaka.media.StreamingEngine.MediaState_} mediaState
* @param {number} playheadTime
* @param {shakaExtern.Period} period
* @param {shakaExtern.Stream} stream
* @param {!shaka.media.SegmentReference} reference
* @param {!ArrayBuffer} segment
* @return {!Promise}
* @private
*/
shaka.media.StreamingEngine.prototype.append_ = function(
mediaState, playheadTime, period, stream, reference, segment) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
return this.evict_(mediaState, playheadTime).then(function() {
if (this.destroyed_) return;
shaka.log.v1(logPrefix, 'appending media segment');
return this.mediaSourceEngine_.appendBuffer(
mediaState.type, segment, reference.startTime + period.startTime,
reference.endTime + period.startTime);
}.bind(this)).then(function() {
if (this.destroyed_) return;
shaka.log.v2(logPrefix, 'appended media segment');
// We must use |stream| because switch() may have been called.
mediaState.lastStream = stream;
mediaState.lastSegmentReference = reference;
return Promise.resolve();
}.bind(this));
};
/**
* Evicts media to meet the max buffer behind limit.
*
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @param {number} playheadTime
* @return {!Promise}
* @private
*/
shaka.media.StreamingEngine.prototype.evict_ = function(
mediaState, playheadTime) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
shaka.log.v2(logPrefix, 'checking buffer length');
var startTime = this.mediaSourceEngine_.bufferStart(mediaState.type);
if (startTime == null) {
shaka.log.v2(logPrefix,
'buffer behind okay because nothing buffered:',
'playheadTime=' + playheadTime,
'bufferBehind=' + this.config_.bufferBehind);
return Promise.resolve();
}
var bufferedBehind = playheadTime - startTime;
var overflow = bufferedBehind - this.config_.bufferBehind;
if (overflow <= 0) {
shaka.log.v2(logPrefix,
'buffer behind okay:',
'playheadTime=' + playheadTime,
'bufferedBehind=' + bufferedBehind,
'bufferBehind=' + this.config_.bufferBehind,
'underflow=' + (-overflow));
return Promise.resolve();
}
shaka.log.v1(logPrefix,
'buffer behind too large:',
'playheadTime=' + playheadTime,
'bufferedBehind=' + bufferedBehind,
'bufferBehind=' + this.config_.bufferBehind,
'overflow=' + overflow);
return this.mediaSourceEngine_.remove(
mediaState.type, startTime, startTime + overflow).then(function() {
if (this.destroyed_) return;
shaka.log.v1(logPrefix, 'evicted ' + overflow + ' seconds');
}.bind(this));
};
/**
* Sets up all known Periods when startup completes; otherwise, does nothing.
*
* @param {shaka.media.StreamingEngine.MediaState_} mediaState The last
* MediaState updated.
* @param {shakaExtern.Stream} stream
* @private
*/
shaka.media.StreamingEngine.prototype.handleStartup_ = function(
mediaState, stream) {
var Functional = shaka.util.Functional;
var MapUtils = shaka.util.MapUtils;
if (this.startupComplete_)
return;
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
goog.asserts.assert(this.mediaStates_, 'must not be destroyed');
var mediaStates = MapUtils.values(this.mediaStates_);
this.startupComplete_ = mediaStates.every(function(ms) {
// Startup completes once we have buffered at least one segment from each
// MediaState, not counting text.
if (ms.type == 'text') return true;
return !ms.waitingToClearBuffer &&
!ms.clearingBuffer &&
ms.lastSegmentReference;
});
if (!this.startupComplete_)
return;
shaka.log.debug(logPrefix, 'startup complete');
// We must use |stream| because switch() may have been called.
var currentPeriodIndex = this.findPeriodContainingStream_(stream);
goog.asserts.assert(
mediaStates.every(function(ms) {
// It is possible for one stream (usually text) to buffer the whole
// Period and need the next one.
return ms.needPeriodIndex == currentPeriodIndex ||
ms.needPeriodIndex == currentPeriodIndex + 1;
}),
logPrefix + ' expected all MediaStates to need same Period');
// Setup the current Period if necessary, which is likely since the current
// Period is probably the initial one.
if (!this.canSwitchPeriod_[currentPeriodIndex]) {
this.setupPeriod_(currentPeriodIndex).then(function() {
shaka.log.v1(logPrefix, 'calling onCanSwitch_()...');
this.onCanSwitch_();
}.bind(this)).catch(Functional.noop);
}
// Now setup all known Periods.
for (var i = 0; i < this.manifest_.periods.length; ++i) {
this.setupPeriod_(i).catch(Functional.noop);
}
if (this.onStartupComplete_) {
shaka.log.v1(logPrefix, 'calling onStartupComplete_()...');
this.onStartupComplete_();
}
};
/**
* Calls onChooseStreams_() when necessary.
*
* @param {shaka.media.StreamingEngine.MediaState_} mediaState The last
* MediaState updated.
* @private
*/
shaka.media.StreamingEngine.prototype.handlePeriodTransition_ = function(
mediaState) {
var Functional = shaka.util.Functional;
var MapUtils = shaka.util.MapUtils;
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
var currentPeriodIndex = this.findPeriodContainingStream_(mediaState.stream);
if (mediaState.needPeriodIndex == currentPeriodIndex)
return;
var needPeriodIndex = mediaState.needPeriodIndex;
goog.asserts.assert(this.mediaStates_, 'must not be destroyed');
var mediaStates = MapUtils.values(this.mediaStates_);
// For a Period transition to work, all media states must need the same
// Period. If a stream needs a different Period than the one it currently
// has, it will try to transition or stop updates assuming that another stream
// will handle it. This only works when all streams either need the same
// Period or are still performing updates.
goog.asserts.assert(
mediaStates.every(function(ms) {
return ms.needPeriodIndex == needPeriodIndex || ms.hasError ||
!shaka.media.StreamingEngine.isIdle_(ms);
}),
'All MediaStates should need the same Period be performing updates.');
// Only call onChooseStreams_() when all MediaStates need the same Period.
var needSamePeriod = mediaStates.every(function(ms) {
return ms.needPeriodIndex == needPeriodIndex;
});
if (!needSamePeriod) {
shaka.log.debug(
logPrefix, 'not all MediaStates need Period ' + needPeriodIndex);
return;
}
// Only call onChooseStreams_() once per Period transition.
var allAreIdle = mediaStates.every(shaka.media.StreamingEngine.isIdle_);
if (!allAreIdle) {
shaka.log.debug(
logPrefix,
'all MediaStates need Period ' + needPeriodIndex + ', ' +
'but not all MediaStates are idle');
return;
}
shaka.log.debug(logPrefix, 'all need Period ' + needPeriodIndex);
// Ensure the Period which we need to buffer is setup and then call
// onChooseStreams_().
this.setupPeriod_(needPeriodIndex).then(function() {
if (this.destroyed_) return;
// If we seek during a Period transition, we can start another transition.
// So we need to verify that:
// - We are still in need of the same Period.
// - All streams are still idle.
// - The current stream is not in the needed Period (another transition
// handled it).
var allReady = mediaStates.every(function(ms) {
var isIdle = shaka.media.StreamingEngine.isIdle_(ms);
var currentPeriodIndex = this.findPeriodContainingStream_(ms.stream);
return isIdle && ms.needPeriodIndex == needPeriodIndex &&
currentPeriodIndex != needPeriodIndex;
}.bind(this));
if (!allReady) {
// TODO: Write unit tests for this case.
shaka.log.debug(logPrefix, 'ignoring transition to Period',
needPeriodIndex, 'since another is happening');
return;
}
var needPeriod = this.manifest_.periods[needPeriodIndex];
shaka.log.v1(logPrefix, 'calling onChooseStreams_()...');
var streamsByType = this.onChooseStreams_(needPeriod);
// Vet |streamsByType| before switching.
for (var type in this.mediaStates_) {
if (streamsByType[type] || type == 'text') continue;
shaka.log.error(logPrefix,
'invalid Streams chosen: missing ' + type + ' Stream');
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.STREAMING,
shaka.util.Error.Code.INVALID_STREAMS_CHOSEN));
return;
}
for (var type in streamsByType) {
if (this.mediaStates_[type]) continue;
if (type == 'text') {
// initStreams_ will switch streams and schedule an update.
this.initStreams_(
{text: streamsByType['text']}, needPeriod.startTime);
delete streamsByType[type];
continue;
}
shaka.log.error(logPrefix,
'invalid Streams chosen: unusable ' + type + ' Stream');
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.STREAMING,
shaka.util.Error.Code.INVALID_STREAMS_CHOSEN));
return;
}
for (var type in this.mediaStates_) {
var stream = streamsByType[type];
if (stream) {
this.switch(type, stream, /* clearBuffer */ false);
this.scheduleUpdate_(this.mediaStates_[type], 0);
} else {
goog.asserts.assert(type == 'text', 'Invalid streams chosen');
delete this.mediaStates_[type];
}
}
// We've already set up the Period so call onCanSwitch_() right now.
shaka.log.v1(logPrefix, 'calling onCanSwitch_()...');
this.onCanSwitch_();
}.bind(this)).catch(Functional.noop);
};
/**
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @return {boolean} True if the given MediaState is idle; otherwise, return
* false.
* @private
*/
shaka.media.StreamingEngine.isIdle_ = function(mediaState) {
return !mediaState.performingUpdate &&
(mediaState.updateTimer == null) &&
!mediaState.waitingToClearBuffer &&
!mediaState.clearingBuffer;
};
/**
* @param {number} time The time, in seconds, relative to the start of the
* presentation.
* @return {number} The index of the Period which starts after |time|
* @private
*/
shaka.media.StreamingEngine.prototype.findPeriodContainingTime_ = function(
time) {
for (var i = this.manifest_.periods.length - 1; i > 0; --i) {
var period = this.manifest_.periods[i];
if (time >= period.startTime)
return i;
}
return 0;
};
/**
* @param {!shakaExtern.Stream} stream
* @return {number} The index of the Period which contains |stream|, or -1 if
* no Period contains |stream|.
* @private
*/
shaka.media.StreamingEngine.prototype.findPeriodContainingStream_ = function(
stream) {
for (var i = 0; i < this.manifest_.periods.length; ++i) {
var period = this.manifest_.periods[i];
for (var j = 0; j < period.streamSets.length; ++j) {
var streamSet = period.streamSets[j];
var index = streamSet.streams.indexOf(stream);
if (index >= 0)
return i;
}
}
return -1;
};
/**
* Fetches the given segment.
*
* @param {(!shaka.media.InitSegmentReference|!shaka.media.SegmentReference)}
* reference
*
* @return {!Promise.<!ArrayBuffer>}
* @private
*/
shaka.media.StreamingEngine.prototype.fetch_ = function(reference) {
var requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
var request = shaka.net.NetworkingEngine.makeRequest(
reference.getUris(), this.config_.retryParameters);
// Set Range header. Note that some web servers don't accept Range headers,
// so don't set one if it's not strictly required.
if ((reference.startByte != 0) || (reference.endByte != null)) {
var range = 'bytes=' + reference.startByte + '-';
if (reference.endByte != null) range += reference.endByte;
request.headers['Range'] = range;
}
shaka.log.v2('fetching: reference=' + reference);
var p = this.netEngine_.request(requestType, request);
return p.then(function(response) {
return response.data;
});
};
/**
* Clears the buffer and schedules another update.
*
* @param {!shaka.media.StreamingEngine.MediaState_} mediaState
* @param {boolean} flush
* @private
*/
shaka.media.StreamingEngine.prototype.clearBuffer_ =
function(mediaState, flush) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
goog.asserts.assert(
!mediaState.performingUpdate && (mediaState.updateTimer == null),
logPrefix + ' unexpected call to clearBuffer_()');
mediaState.waitingToClearBuffer = false;
mediaState.waitingToFlushBuffer = false;
mediaState.clearingBuffer = true;
shaka.log.debug(logPrefix, 'clearing buffer');
var p = this.mediaSourceEngine_.clear(mediaState.type);
p.then(function() {
if (!this.destroyed_ && flush) {
return this.mediaSourceEngine_.flush(mediaState.type);
}
}.bind(this)).then(function() {
if (this.destroyed_) return;
shaka.log.debug(logPrefix, 'cleared buffer');
mediaState.lastStream = null;
mediaState.lastSegmentReference = null;
mediaState.clearingBuffer = false;
this.scheduleUpdate_(mediaState, 0);
}.bind(this));
};
/**
* Schedules |mediaState|'s next update.
*
* @param {!shaka.media.StreamingEngine.MediaState_} mediaState
* @param {number} delay The delay in seconds.
* @private
*/
shaka.media.StreamingEngine.prototype.scheduleUpdate_ = function(
mediaState, delay) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
shaka.log.v2(logPrefix, 'updating in ' + delay + ' seconds');
goog.asserts.assert(mediaState.updateTimer == null,
logPrefix + ' did not expect update to be scheduled');
mediaState.updateTimer = window.setTimeout(
this.onUpdate_.bind(this, mediaState), delay * 1000);
};
/**
* Cancels |mediaState|'s next update if one exists.
*
* @param {!shaka.media.StreamingEngine.MediaState_} mediaState
* @private
*/
shaka.media.StreamingEngine.prototype.cancelUpdate_ = function(mediaState) {
if (mediaState.updateTimer != null) {
window.clearTimeout(mediaState.updateTimer);
mediaState.updateTimer = null;
}
};
/**
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @return {string} A log prefix of the form ($CONTENT_TYPE:$STREAM_ID), e.g.,
* "(audio:5)" or "(video:hd)".
* @private
*/
shaka.media.StreamingEngine.logPrefix_ = function(mediaState) {
return '(' + mediaState.type + ':' + mediaState.stream.id + ')';
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.TextEngine');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.util.IDestroyable');
/**
* Manages text parsers and cues.
*
* @struct
* @constructor
* @param {TextTrack} track
* @param {string} mimeType
* @param {boolean} useRelativeCueTimestamps
* @implements {shaka.util.IDestroyable}
*/
shaka.media.TextEngine = function(track, mimeType, useRelativeCueTimestamps) {
/** @private {?shakaExtern.TextParserPlugin} */
this.parser_ = shaka.media.TextEngine.parserMap_[mimeType];
// This should not happen if type negotiation is working as it should.
goog.asserts.assert(this.parser_,
'Text type negotiation should have happened already');
/** @private {TextTrack} */
this.track_ = track;
/** @private {number} */
this.timestampOffset_ = 0;
/** @private {number} */
this.appendWindowEnd_ = Infinity;
/** @private {?number} */
this.bufferStart_ = null;
/** @private {?number} */
this.bufferEnd_ = null;
/** @private {boolean} */
this.useRelativeCueTimestamps_ = useRelativeCueTimestamps;
};
/** @private {!Object.<string, shakaExtern.TextParserPlugin>} */
shaka.media.TextEngine.parserMap_ = {};
/**
* @param {string} mimeType
* @param {shakaExtern.TextParserPlugin} parser
* @export
*/
shaka.media.TextEngine.registerParser = function(mimeType, parser) {
shaka.media.TextEngine.parserMap_[mimeType] = parser;
};
/**
* @param {string} mimeType
* @export
*/
shaka.media.TextEngine.unregisterParser = function(mimeType) {
delete shaka.media.TextEngine.parserMap_[mimeType];
};
/**
* @param {string} mimeType
* @return {boolean}
*/
shaka.media.TextEngine.isTypeSupported = function(mimeType) {
return !!shaka.media.TextEngine.parserMap_[mimeType];
};
/**
* Creates a cue using the best platform-specific interface available.
*
* @param {number} startTime
* @param {number} endTime
* @param {string} payload
* @return {TextTrackCue} or null if the parameters were invalid.
* @export
*/
shaka.media.TextEngine.makeCue = function(startTime, endTime, payload) {
if (startTime >= endTime) {
// IE/Edge will throw in this case.
// See issue #501
shaka.log.warning('Invalid cue times: ' + startTime + ' - ' + endTime);
return null;
}
return new VTTCue(startTime, endTime, payload);
};
/** @override */
shaka.media.TextEngine.prototype.destroy = function() {
if (this.track_) {
this.removeWhere_(function(cue) { return true; });
}
this.parser_ = null;
this.track_ = null;
return Promise.resolve();
};
/**
* @param {ArrayBuffer} buffer
* @param {?number} startTime
* @param {?number} endTime
* @return {!Promise}
*/
shaka.media.TextEngine.prototype.appendBuffer =
function(buffer, startTime, endTime) {
var offset = this.timestampOffset_;
// Start the operation asynchronously to avoid blocking the caller.
return Promise.resolve().then(function() {
// Check that TextEngine hasn't been destroyed.
if (!this.track_) return;
// Parse the buffer and add the new cues.
var cues = this.parser_(buffer,
offset,
startTime,
endTime,
this.useRelativeCueTimestamps_);
if (startTime == null || endTime == null) {
// Init segments will not have start/end times passed
return;
}
for (var i = 0; i < cues.length; ++i) {
if (cues[i].startTime >= this.appendWindowEnd_) break;
this.track_.addCue(cues[i]);
}
// NOTE: We update the buffered range from the start and end times passed
// down from the segment reference, not with the start and end times of the
// parsed cues. This is important because some segments may contain no
// cues, but we must still consider those ranges buffered.
if (this.bufferStart_ == null) {
this.bufferStart_ = startTime;
} else {
// We already had something in buffer, and we assume we are extending the
// range from the end.
goog.asserts.assert((startTime - this.bufferEnd_) <= 1,
'There should not be a gap in text references >1s');
}
this.bufferEnd_ = Math.min(endTime, this.appendWindowEnd_);
}.bind(this));
};
/**
* @param {number} start
* @param {number} end
* @return {!Promise}
*/
shaka.media.TextEngine.prototype.remove = function(start, end) {
// Start the operation asynchronously to avoid blocking the caller.
return Promise.resolve().then(function() {
// Check that TextEngine hasn't been destroyed.
if (!this.track_) return;
this.removeWhere_(function(cue) {
if (cue.startTime >= end || cue.endTime <= start) {
// Outside the remove range. Hang on to it.
return false;
}
return true;
});
if (this.bufferStart_ == null) {
goog.asserts.assert(this.bufferEnd_ == null,
'end must be null if start is null');
} else {
goog.asserts.assert(this.bufferEnd_ != null,
'end must be non-null if start is non-null');
// Update buffered range.
if (end <= this.bufferStart_ || start >= this.bufferEnd_) {
// No intersection. Nothing was removed.
} else if (start <= this.bufferStart_ && end >= this.bufferEnd_) {
// We wiped out everything.
goog.asserts.assert(
this.track_.cues.length == 0, 'should be no cues left');
this.bufferStart_ = this.bufferEnd_ = null;
} else if (start <= this.bufferStart_ && end < this.bufferEnd_) {
// We removed from the beginning of the range.
this.bufferStart_ = end;
} else if (start > this.bufferStart_ && end >= this.bufferEnd_) {
// We removed from the end of the range.
this.bufferEnd_ = start;
} else {
// We removed from the middle? StreamingEngine isn't supposed to.
goog.asserts.assert(
false, 'removal from the middle is not supported by TextEngine');
}
}
}.bind(this));
};
/** @param {number} timestampOffset */
shaka.media.TextEngine.prototype.setTimestampOffset =
function(timestampOffset) {
this.timestampOffset_ = timestampOffset;
};
/** @param {number} windowEnd */
shaka.media.TextEngine.prototype.setAppendWindowEnd =
function(windowEnd) {
this.appendWindowEnd_ = windowEnd;
};
/**
* @return {?number} Time in seconds of the beginning of the buffered range,
* or null if nothing is buffered.
*/
shaka.media.TextEngine.prototype.bufferStart = function() {
return this.bufferStart_;
};
/**
* @return {?number} Time in seconds of the end of the buffered range,
* or null if nothing is buffered.
*/
shaka.media.TextEngine.prototype.bufferEnd = function() {
return this.bufferEnd_;
};
/**
* @param {number} t A timestamp
* @return {number} Number of seconds ahead of 't' we have buffered
*/
shaka.media.TextEngine.prototype.bufferedAheadOf = function(t) {
if (this.bufferEnd_ == null || this.bufferEnd_ < t) return 0;
goog.asserts.assert(
this.bufferStart_ != null, 'start should not be null if end is not null');
if (t < this.bufferStart_) return 0;
return this.bufferEnd_ - t;
};
/**
* Remove all cues for which the matching function returns true.
*
* @param {function(!TextTrackCue):boolean} predicate
* @private
*/
shaka.media.TextEngine.prototype.removeWhere_ = function(predicate) {
var cues = this.track_.cues;
var removeMe = [];
// Remove these in another loop to avoid mutating the TextTrackCueList
// while iterating over it. This allows us to avoid making assumptions
// about whether or not this.track_.remove() will alter that list.
for (var i = 0; i < cues.length; ++i) {
if (predicate(cues[i])) {
removeMe.push(cues[i]);
}
}
for (var i = 0; i < removeMe.length; ++i) {
this.track_.removeCue(removeMe[i]);
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.TimeRangesUtils');
/**
* @namespace shaka.media.TimeRangesUtils
* @summary A set of utility functions for dealing with TimeRanges objects.
*/
/**
* Gets the first timestamp in buffer.
*
* @param {TimeRanges} b
* @return {?number} The first buffered timestamp, in seconds, if |buffered|
* is non-empty; otherwise, return null.
*/
shaka.media.TimeRangesUtils.bufferStart = function(b) {
if (!b) return null;
// Workaround Safari bug: https://goo.gl/EDRCoZ
if (b.length == 1 && b.end(0) - b.start(0) < 1e-6) return null;
// Workaround Edge bug: https://goo.gl/BtxKgb
if (b.length == 1 && b.start(0) < 0) return 0;
return b.length ? b.start(0) : null;
};
/**
* Gets the last timestamp in buffer.
*
* @param {TimeRanges} b
* @return {?number} The last buffered timestamp, in seconds, if |buffered|
* is non-empty; otherwise, return null.
*/
shaka.media.TimeRangesUtils.bufferEnd = function(b) {
if (!b) return null;
// Workaround Safari bug: https://goo.gl/EDRCoZ
if (b.length == 1 && b.end(0) - b.start(0) < 1e-6) return null;
return b.length ? b.end(b.length - 1) : null;
};
/**
* Computes how far ahead of the given timestamp is buffered.
*
* @param {TimeRanges} b
* @param {number} time
* @return {number} The number of seconds buffered, in seconds, ahead of the
* given time.
*/
shaka.media.TimeRangesUtils.bufferedAheadOf = function(b, time) {
var result = 0;
if (!b) return result;
// Workaround Safari bug: https://goo.gl/EDRCoZ
if (b.length == 1 && b.end(0) - b.start(0) < 1e-6) return result;
var gapTolerance = shaka.media.TimeRangesUtils.GAP_TOLERANCE;
var isInRange = false;
// NOTE: On IE11, buffered ranges may show appended data before the associated
// append operation is complete.
var fudge = 0.0001; // 0.1ms
// NOTE: The 0.1ms fudge is needed on Safari, where removal up to X may leave
// a range which starts at X + 1us + some small epsilon.
if (time == 0) {
// Browsers seem willing to tolerate larger gaps at the start of the media.
// If we're asking what's buffered from 0, tolerate a large gap and expect
// browsers to do the same. This way, we will not artificially block media
// that the browser could otherwise play.
fudge = 0.25; // 250ms
}
for (var i = 0; i < b.length; ++i) {
if (time + fudge >= b.start(i) && time < b.end(i)) {
result += b.end(i) - time;
isInRange = true;
} else if (isInRange &&
(b.start(i) - b.end(i - 1)) <= gapTolerance) {
// Jump small gaps in media and treat two intervals with a small gap as
// one consecutive interval.
result += b.end(i) - b.start(i);
result += b.start(i) - b.end(i - 1);
} else if (i > 0 && time + fudge < b.start(i) &&
time + fudge >= b.end(i - 1)) {
// We're seeking to a point inside the gap.
if (b.start(i) - time <= gapTolerance) {
// If the gap is small enough, jump it
result += b.end(i) - time;
isInRange = true;
} else {
return result;
}
} else {
isInRange = false;
}
}
return result;
};
/**
* Computes the amount buffered ahead, allowing a gap of the given size at the
* beginning.
*
* @param {TimeRanges} b
* @param {number} time
* @param {number} tolerance The size of the allowed gap, in seconds.
* @return {number} The amount of content buffered, in seconds.
*/
shaka.media.TimeRangesUtils.bufferedAheadOfThreshold = function(
b, time, tolerance) {
var TimeRangesUtils = shaka.media.TimeRangesUtils;
var bufferedAhead = TimeRangesUtils.bufferedAheadOf(b, time);
if (!bufferedAhead) {
bufferedAhead = TimeRangesUtils.bufferedAheadOf(b, time + tolerance);
if (bufferedAhead) bufferedAhead += tolerance;
}
return bufferedAhead;
};
/** @const {number} */
shaka.media.TimeRangesUtils.GAP_TOLERANCE = 0.04; // 40 ms
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.TtmlTextParser');
goog.require('goog.asserts');
goog.require('shaka.media.TextEngine');
goog.require('shaka.util.Error');
goog.require('shaka.util.StringUtils');
/**
* @namespace
* @summary A TextEngine plugin that parses TTML files.
* @param {ArrayBuffer} data
* @param {number} offset
* @param {?number} segmentStartTime
* @param {?number} segmentEndTime
* @param {boolean} useRelativeCueTimestamps Only used by the VTT parser
* @return {!Array.<!TextTrackCue>}
* @throws {shaka.util.Error}
* @export
*/
shaka.media.TtmlTextParser =
function(data, offset, segmentStartTime,
segmentEndTime, useRelativeCueTimestamps) {
var str = shaka.util.StringUtils.fromUTF8(data);
var ret = [];
var parser = new DOMParser();
var xml = null;
try {
xml = parser.parseFromString(str, 'text/xml');
} catch (exception) {
throw new shaka.util.Error(
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_XML);
}
if (xml) {
// Try to get the framerate, subFrameRate and frameRateMultiplier
// if applicable
var frameRate = null;
var subFrameRate = null;
var frameRateMultiplier = null;
var tickRate = null;
var spaceStyle = null;
var tts = xml.getElementsByTagName('tt');
var tt = tts[0];
// TTML should always have tt element
if (!tt) {
throw new shaka.util.Error(
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_TTML);
} else {
frameRate = tt.getAttribute('ttp:frameRate');
subFrameRate = tt.getAttribute('ttp:subFrameRate');
frameRateMultiplier = tt.getAttribute('ttp:frameRateMultiplier');
tickRate = tt.getAttribute('ttp:tickRate');
spaceStyle = tt.getAttribute('xml:space') || 'default';
}
if (spaceStyle != 'default' && spaceStyle != 'preserve') {
throw new shaka.util.Error(
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_XML);
}
var whitespaceTrim = spaceStyle == 'default';
var rateInfo = new shaka.media.TtmlTextParser.RateInfo_(
frameRate, subFrameRate, frameRateMultiplier, tickRate);
var styles = shaka.media.TtmlTextParser.getLeafNodes_(
tt.getElementsByTagName('styling')[0]);
var regions = shaka.media.TtmlTextParser.getLeafNodes_(
tt.getElementsByTagName('layout')[0]);
var textNodes = shaka.media.TtmlTextParser.getLeafNodes_(
tt.getElementsByTagName('body')[0]);
for (var i = 0; i < textNodes.length; i++) {
var cue = shaka.media.TtmlTextParser.parseCue_(
textNodes[i], offset, rateInfo, styles, regions, whitespaceTrim);
if (cue)
ret.push(cue);
}
}
return ret;
};
/**
* @const
* @private {!RegExp}
* @example 00:00:40:07 (7 frames) or 00:00:40:07.1 (7 frames, 1 subframe)
*/
shaka.media.TtmlTextParser.timeColonFormatFrames_ =
/^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/;
/**
* @const
* @private {!RegExp}
* @example 00:00:40 or 00:40
*/
shaka.media.TtmlTextParser.timeColonFormat_ =
/^(?:(\d{2,}):)?(\d{2}):(\d{2})$/;
/**
* @const
* @private {!RegExp}
* example 01:02:43.0345555 or 02:43.03
*/
shaka.media.TtmlTextParser.timeColonFormatMilliseconds_ =
/^(?:(\d{2,}):)?(\d{2}):(\d{2}\.\d{2,})$/;
/**
* @const
* @private {!RegExp}
* @example 75f or 75.5f
*/
shaka.media.TtmlTextParser.timeFramesFormat_ = /^(\d*\.?\d*)f$/;
/**
* @const
* @private {!RegExp}
* @example 50t or 50.5t
*/
shaka.media.TtmlTextParser.timeTickFormat_ = /^(\d*\.?\d*)t$/;
/**
* @const
* @private {!RegExp}
* @example 3.45h, 3m or 4.20s
*/
shaka.media.TtmlTextParser.timeHMSFormat_ =
/^(?:(\d*\.?\d*)h)?(?:(\d*\.?\d*)m)?(?:(\d*\.?\d*)s)?(?:(\d*\.?\d*)ms)?$/;
/**
* @const
* @private {!RegExp}
* @example 50% 10%
*/
shaka.media.TtmlTextParser.percentValues_ = /^(\d{1,2}|100)% (\d{1,2}|100)%$/;
/**
* @const
* @private {!Object}
*/
shaka.media.TtmlTextParser.textAlignToLineAlign_ = {
'left': 'start',
'center': 'center',
'right': 'end',
'start': 'start',
'end': 'end'
};
/**
* @const
* @private {!Object}
*/
shaka.media.TtmlTextParser.textAlignToPositionAlign_ = {
'left': 'line-left',
'center': 'center',
'right': 'line-right'
};
/**
* Gets leaf nodes of the xml node tree. Ignores the text, br elements
* and the spans positioned inside paragraphs
*
* @param {Element} element
* @return {!Array.<!Element>}
* @private
*/
shaka.media.TtmlTextParser.getLeafNodes_ = function(element) {
var result = [];
if (!element)
return result;
var childNodes = element.childNodes;
for (var i = 0; i < childNodes.length; i++) {
// Currently we don't support styles applicable to span
// elements, so they are ignored
var isSpanChildOfP = childNodes[i].nodeName == 'span' &&
element.nodeName == 'p';
if (childNodes[i].nodeType == Node.ELEMENT_NODE &&
childNodes[i].nodeName != 'br' && !isSpanChildOfP) {
// Get the leafs the child might contain
goog.asserts.assert(childNodes[i] instanceof Element,
'Node should be Element!');
var leafChildren = shaka.media.TtmlTextParser.getLeafNodes_(
/** @type {Element} */(childNodes[i]));
goog.asserts.assert(leafChildren.length > 0,
'Only a null Element should return no leaves!');
result = result.concat(leafChildren);
}
}
// if no result at this point, the element itself must be a leaf
if (!result.length) {
result.push(element);
}
return result;
};
/**
* Insert \n where <br> tags are found
*
* @param {!Node} element
* @param {boolean} whitespaceTrim
* @private
*/
shaka.media.TtmlTextParser.addNewLines_ = function(element, whitespaceTrim) {
var childNodes = element.childNodes;
for (var i = 0; i < childNodes.length; i++) {
if (childNodes[i].nodeName == 'br' && i > 0) {
childNodes[i - 1].textContent += '\n';
} else if (childNodes[i].childNodes.length > 0) {
shaka.media.TtmlTextParser.addNewLines_(childNodes[i], whitespaceTrim);
} else if (whitespaceTrim) {
// Trim leading and trailing whitespace.
var trimmed = childNodes[i].textContent.trim();
// Collapse multiple spaces into one.
trimmed = trimmed.replace(/\s+/g, ' ');
childNodes[i].textContent = trimmed;
}
}
};
/**
* Parses an Element into a TextTrackCue or VTTCue.
*
* @param {!Element} cueElement
* @param {number} offset
* @param {!shaka.media.TtmlTextParser.RateInfo_} rateInfo
* @param {!Array.<!Element>} styles
* @param {!Array.<!Element>} regions
* @param {boolean} whitespaceTrim
* @return {TextTrackCue}
* @private
*/
shaka.media.TtmlTextParser.parseCue_ = function(
cueElement, offset, rateInfo, styles, regions, whitespaceTrim) {
// Disregard empty elements:
// TTML allows for empty elements like <div></div>.
// If cueElement has neither time attributes, nor
// non-whitespace text, don't try to make a cue out of it.
if (!cueElement.hasAttribute('begin') &&
!cueElement.hasAttribute('end') &&
/^\s*$/.test(cueElement.textContent))
return null;
shaka.media.TtmlTextParser.addNewLines_(cueElement, whitespaceTrim);
// Get time
var start = shaka.media.TtmlTextParser.parseTime_(
cueElement.getAttribute('begin'), rateInfo);
var end = shaka.media.TtmlTextParser.parseTime_(
cueElement.getAttribute('end'), rateInfo);
var duration = shaka.media.TtmlTextParser.parseTime_(
cueElement.getAttribute('dur'), rateInfo);
var payload = cueElement.textContent;
if (end == null && duration != null)
end = start + duration;
if (start == null || end == null) {
throw new shaka.util.Error(
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_TEXT_CUE);
}
start += offset;
end += offset;
var cue = shaka.media.TextEngine.makeCue(start, end, payload);
if (!cue)
return null;
// Get other properties if available
var region = shaka.media.TtmlTextParser.getElementFromCollection_(
cueElement, 'region', regions);
shaka.media.TtmlTextParser.addStyle_(cue, cueElement, region, styles);
return cue;
};
/**
* Adds applicable style properties to a cue.
*
* @param {!TextTrackCue} cue
* @param {!Element} cueElement
* @param {Element} region
* @param {!Array.<!Element>} styles
* @private
*/
shaka.media.TtmlTextParser.addStyle_ = function(
cue, cueElement, region, styles) {
var TtmlTextParser = shaka.media.TtmlTextParser;
var results = null;
var extent = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'tts:extent');
if (extent) {
results = TtmlTextParser.percentValues_.exec(extent);
if (results != null) {
// Use width value of the extent attribute for size.
// Height value is ignored.
cue.size = Number(results[1]);
}
}
var writingMode = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'tts:writingMode');
var isVerticalText = true;
if (writingMode == 'tb' || writingMode == 'tblr')
cue.vertical = 'lr';
else if (writingMode == 'tbrl')
cue.vertical = 'rl';
else
isVerticalText = false;
var origin = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'tts:origin');
if (origin) {
results = TtmlTextParser.percentValues_.exec(origin);
if (results != null) {
// for vertical text use first coordinate of tts:origin
// to represent line of the cue and second - for position.
// Otherwise (horizontal), use them the other way around.
if (isVerticalText) {
cue.position = Number(results[2]);
cue.line = Number(results[1]);
} else {
cue.position = Number(results[1]);
cue.line = Number(results[2]);
}
// A boolean indicating whether the line is an integer
// number of lines (using the line dimensions of the first
// line of the cue), or whether it is a percentage of the
// dimension of the video. The flag is set to true when lines
// are counted, and false otherwise.
cue.snapToLines = false;
}
}
var align = TtmlTextParser.getStyleAttribute_(
cueElement, region, styles, 'tts:textAlign');
if (align) {
cue.align = align;
if (align == 'center') {
if (cue.align != 'center') {
// Workaround for a Chrome bug http://crbug.com/663797
// Chrome does not support align = 'center'
cue.align = 'middle';
}
cue.position = 'auto';
}
cue.positionAlign = TtmlTextParser.textAlignToPositionAlign_[align];
cue.lineAlign = TtmlTextParser.textAlignToLineAlign_[align];
}
};
/**
* Finds a specified attribute on either the original cue element or its
* associated region and returns the value if the attribute was found.
*
* @param {!Element} cueElement
* @param {Element} region
* @param {!Array.<!Element>} styles
* @param {string} attribute
* @return {?string}
* @private
*/
shaka.media.TtmlTextParser.getStyleAttribute_ = function(
cueElement, region, styles, attribute) {
// An attribute can be specified on region level or in a styling block
// associated with the region or original element.
var regionChildren = shaka.media.TtmlTextParser.getLeafNodes_(region);
for (var i = 0; i < regionChildren.length; i++) {
var attr = regionChildren[i].getAttribute(attribute);
if (attr)
return attr;
}
var getElementFromCollection_ =
shaka.media.TtmlTextParser.getElementFromCollection_;
var style = getElementFromCollection_(region, 'style', styles) ||
getElementFromCollection_(cueElement, 'style', styles);
if (style)
return style.getAttribute(attribute);
return null;
};
/**
* Selects an item from |collection| whose id matches |attributeName|
* from |element|.
*
* @param {Element} element
* @param {string} attributeName
* @param {!Array.<Element>} collection
* @return {Element}
* @private
*/
shaka.media.TtmlTextParser.getElementFromCollection_ = function(
element, attributeName, collection) {
if (!element || collection.length < 1) {
return null;
}
var item = null;
var itemName = shaka.media.TtmlTextParser.getInheritedAttribute_(
element, attributeName);
if (itemName) {
for (var i = 0; i < collection.length; i++) {
if (collection[i].getAttribute('xml:id') == itemName) {
item = collection[i];
break;
}
}
}
return item;
};
/**
* Traverses upwards from a given node until a given attribute is found.
*
* @param {!Element} element
* @param {string} attributeName
* @return {?string}
* @private
*/
shaka.media.TtmlTextParser.getInheritedAttribute_ = function(
element, attributeName) {
var ret = null;
while (element) {
ret = element.getAttribute(attributeName);
if (ret) {
break;
}
// Element.parentNode can lead to XMLDocument, which is not an Element and
// has no getAttribute().
var parentNode = element.parentNode;
if (parentNode instanceof Element) {
element = parentNode;
} else {
break;
}
}
return ret;
};
/**
* Parses a TTML time from the given word.
*
* @param {string} text
* @param {!shaka.media.TtmlTextParser.RateInfo_} rateInfo
* @return {?number}
* @private
*/
shaka.media.TtmlTextParser.parseTime_ = function(text, rateInfo) {
var ret = null;
var TtmlTextParser = shaka.media.TtmlTextParser;
if (TtmlTextParser.timeColonFormatFrames_.test(text)) {
ret = TtmlTextParser.parseColonTimeWithFrames_(rateInfo, text);
} else if (TtmlTextParser.timeColonFormat_.test(text)) {
ret = TtmlTextParser.parseTimeFromRegex_(
TtmlTextParser.timeColonFormat_, text);
} else if (TtmlTextParser.timeColonFormatMilliseconds_.test(text)) {
ret = TtmlTextParser.parseTimeFromRegex_(
TtmlTextParser.timeColonFormatMilliseconds_, text);
} else if (TtmlTextParser.timeFramesFormat_.test(text)) {
ret = TtmlTextParser.parseFramesTime_(rateInfo, text);
} else if (TtmlTextParser.timeTickFormat_.test(text)) {
ret = TtmlTextParser.parseTickTime_(rateInfo, text);
} else if (TtmlTextParser.timeHMSFormat_.test(text)) {
ret = TtmlTextParser.parseTimeFromRegex_(
TtmlTextParser.timeHMSFormat_, text);
}
return ret;
};
/**
* Parses a TTML time in frame format
*
* @param {!shaka.media.TtmlTextParser.RateInfo_} rateInfo
* @param {string} text
* @return {?number}
* @private
*/
shaka.media.TtmlTextParser.parseFramesTime_ = function(rateInfo, text) {
// 75f or 75.5f
var results = shaka.media.TtmlTextParser.timeFramesFormat_.exec(text);
var frames = Number(results[1]);
return frames / rateInfo.frameRate;
};
/**
* Parses a TTML time in tick format
*
* @param {!shaka.media.TtmlTextParser.RateInfo_} rateInfo
* @param {string} text
* @return {?number}
* @private
*/
shaka.media.TtmlTextParser.parseTickTime_ = function(rateInfo, text) {
// 50t or 50.5t
var results = shaka.media.TtmlTextParser.timeTickFormat_.exec(text);
var ticks = Number(results[1]);
return ticks / rateInfo.tickRate;
};
/**
* Parses a TTML colon formatted time containing frames
*
* @param {!shaka.media.TtmlTextParser.RateInfo_} rateInfo
* @param {string} text
* @return {?number}
* @private
*/
shaka.media.TtmlTextParser.parseColonTimeWithFrames_ = function(
rateInfo, text) {
// 01:02:43:07 ('07' is frames) or 01:02:43:07.1 (subframes)
var results = shaka.media.TtmlTextParser.timeColonFormatFrames_.exec(text);
var hours = Number(results[1]);
var minutes = Number(results[2]);
var seconds = Number(results[3]);
var frames = Number(results[4]);
var subframes = Number(results[5]) || 0;
frames += subframes / rateInfo.subFrameRate;
seconds += frames / rateInfo.frameRate;
return seconds + (minutes * 60) + (hours * 3600);
};
/**
* Parses a TTML time with a given regex. Expects regex to be some
* sort of a time-matcher to match hours, minutes, seconds and milliseconds
*
* @param {!RegExp} regex
* @param {string} text
* @return {?number}
* @private
*/
shaka.media.TtmlTextParser.parseTimeFromRegex_ = function(regex, text) {
var results = regex.exec(text);
if (results == null || results[0] == '')
return null;
// This capture is optional, but will still be in the array as undefined,
// default to 0.
var hours = Number(results[1]) || 0;
var minutes = Number(results[2]) || 0;
var seconds = Number(results[3]) || 0;
var miliseconds = Number(results[4]) || 0;
return (miliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
};
/**
* Contains information about frame/subframe rate
* and frame rate multiplier for time in frame format.
* ex. 01:02:03:04(4 frames) or 01:02:03:04.1(4 frames, 1 subframe)
*
* @param {?string} frameRate
* @param {?string} subFrameRate
* @param {?string} frameRateMultiplier
* @param {?string} tickRate
* @constructor
* @struct
* @private
*/
shaka.media.TtmlTextParser.RateInfo_ = function(
frameRate, subFrameRate, frameRateMultiplier, tickRate) {
/**
* @type {number}
*/
this.frameRate = Number(frameRate) || 30;
/**
* @type {number}
*/
this.subFrameRate = Number(subFrameRate) || 1;
/**
* @type {number}
*/
this.tickRate = Number(tickRate);
if (this.tickRate == 0) {
if (frameRate)
this.tickRate = this.frameRate * this.subFrameRate;
else
this.tickRate = 1;
}
if (frameRateMultiplier) {
var multiplierResults = /^(\d+) (\d+)$/g.exec(frameRateMultiplier);
if (multiplierResults) {
var numerator = multiplierResults[1];
var denominator = multiplierResults[2];
var multiplierNum = numerator / denominator;
this.frameRate *= multiplierNum;
}
}
};
shaka.media.TextEngine.registerParser(
'application/ttml+xml', shaka.media.TtmlTextParser);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.VttTextParser');
goog.require('shaka.log');
goog.require('shaka.media.TextEngine');
goog.require('shaka.util.Error');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.TextParser');
/**
* @namespace
* @summary A TextEngine plugin that parses WebVTT files.
* @param {ArrayBuffer} data
* @param {number} offset
* @param {?number} segmentStartTime
* @param {?number} segmentEndTime
* @param {boolean} useRelativeCueTimestamps
* @return {!Array.<!TextTrackCue>}
* @throws {shaka.util.Error}
* @export
*/
shaka.media.VttTextParser =
function(data, offset, segmentStartTime,
segmentEndTime, useRelativeCueTimestamps) {
if (segmentStartTime > 0 && !useRelativeCueTimestamps) {
shaka.log.warning('Period-relative text cue timestamps have been ' +
'deprecated. Segment-relative timestamps will be used ' +
'in V2.1.0 and on.');
}
// Get the input as a string. Normalize newlines to \n.
var str = shaka.util.StringUtils.fromUTF8(data);
str = str.replace(/\r\n|\r(?=[^\n]|$)/gm, '\n');
var blocks = str.split(/\n{2,}/m);
if (!/^WEBVTT($|[ \t\n])/m.test(blocks[0])) {
throw new shaka.util.Error(
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_TEXT_HEADER);
}
var ret = [];
for (var i = 1; i < blocks.length; i++) {
var lines = blocks[i].split('\n');
var cue = shaka.media.VttTextParser.parseCue_(lines,
offset,
segmentStartTime,
useRelativeCueTimestamps);
if (cue)
ret.push(cue);
}
return ret;
};
/**
* Parses a text block into a Cue object.
*
* @param {!Array.<string>} text
* @param {number} offset
* @param {?number} segmentStartTime
* @param {boolean} useRelativeCueTimestamps
* @return {?TextTrackCue}
* @private
*/
shaka.media.VttTextParser.parseCue_ =
function(text, offset, segmentStartTime, useRelativeCueTimestamps) {
// Skip empty blocks.
if (text.length == 1 && !text[0])
return null;
// Skip comment blocks.
if (/^NOTE($|[ \t])/.test(text[0]))
return null;
var id = null;
var index = text[0].indexOf('-->');
if (index < 0) {
id = text[0];
text.splice(0, 1);
}
// Parse the times.
var parser = new shaka.util.TextParser(text[0]);
var start = shaka.media.VttTextParser.parseTime_(parser);
var expect = parser.readRegex(/[ \t]+-->[ \t]+/g);
var end = shaka.media.VttTextParser.parseTime_(parser);
if (start == null || expect == null || end == null) {
throw new shaka.util.Error(
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.INVALID_TEXT_CUE);
}
// See issue #480 for discussion on deprecation
if (useRelativeCueTimestamps) {
start += segmentStartTime;
end += segmentStartTime;
} else {
start += offset;
end += offset;
}
// Get the payload.
var payload = text.slice(1).join('\n').trim();
var cue = shaka.media.TextEngine.makeCue(start, end, payload);
if (!cue)
return null;
// Parse optional settings.
parser.skipWhitespace();
var word = parser.readWord();
while (word) {
if (!shaka.media.VttTextParser.parseSetting(cue, word)) {
shaka.log.warning('VTT parser encountered an invalid VTT setting: ',
word,
' The setting will be ignored.');
}
parser.skipWhitespace();
word = parser.readWord();
}
if (id != null)
cue.id = id;
return cue;
};
/**
* Parses a WebVTT setting from the given word.
*
* @param {!TextTrackCue} cue
* @param {string} word
* @return {boolean} True on success.
*/
shaka.media.VttTextParser.parseSetting = function(cue, word) {
// NOTE: positionAlign and lineAlign settings are not supported by Chrome
// at the moment, so setting them will have no effect.
// The bug on chromium to implement them:
// https://bugs.chromium.org/p/chromium/issues/detail?id=633690
var results = null;
if ((results = /^align:(start|middle|center|end|left|right)$/.exec(word))) {
cue.align = results[1];
if (results[1] == 'center' && cue.align != 'center') {
// Workaround for a Chrome bug http://crbug.com/663797
// Chrome does not support align = 'center'
cue.position = 'auto';
cue.align = 'middle';
}
} else if ((results = /^vertical:(lr|rl)$/.exec(word))) {
cue.vertical = results[1];
} else if ((results = /^size:(\d{1,2}|100)%$/.exec(word))) {
cue.size = Number(results[1]);
}
// There was a disagreement between a working draft and an editor draft of
// the WebVTT spec. According to the former, optional position alignment
// options are 'start', 'end' and 'center'. According to the latter -
// 'line-left', 'center' and 'line-right'.
// We are going to support both options for now.
else if ((results =
/^position:(\d{1,2}|100)%(?:,(line-left|line-right|center|start|end))?$/
.exec(word))) {
cue.position = Number(results[1]);
if (results[2])
cue.positionAlign = results[2];
} else if ((results =
/^line:(\d{1,2}|100)%(?:,(start|end|center))?$/.exec(word))) {
cue.snapToLines = false;
cue.line = Number(results[1]);
if (results[2])
cue.lineAlign = results[2];
} else if ((results = /^line:(-?\d+)(?:,(start|end|center))?$/.exec(word))) {
cue.snapToLines = true;
cue.line = Number(results[1]);
if (results[2])
cue.lineAlign = results[2];
} else {
return false;
}
return true;
};
/**
* Parses a WebVTT time from the given parser.
*
* @param {!shaka.util.TextParser} parser
* @return {?number}
* @private
*/
shaka.media.VttTextParser.parseTime_ = function(parser) {
// 00:00.000 or 00:00:00.000 or 0:00:00.000
var results = parser.readRegex(/(?:(\d{1,}):)?(\d{2}):(\d{2})\.(\d{3})/g);
if (results == null)
return null;
// This capture is optional, but will still be in the array as undefined,
// default to 0.
var hours = Number(results[1]) || 0;
var minutes = Number(results[2]);
var seconds = Number(results[3]);
var miliseconds = Number(results[4]);
if (minutes > 59 || seconds > 59)
return null;
return (miliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
};
shaka.media.TextEngine.registerParser('text/vtt', shaka.media.VttTextParser);
shaka.media.TextEngine.registerParser('text/vtt; codecs="vtt"',
shaka.media.VttTextParser);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.WebmSegmentIndexParser');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.util.EbmlElement');
goog.require('shaka.util.EbmlParser');
goog.require('shaka.util.Error');
/**
* Creates a WebM Cues element parser.
*
* @constructor
* @struct
*/
shaka.media.WebmSegmentIndexParser = function() {};
/** @const {number} */
shaka.media.WebmSegmentIndexParser.EBML_ID = 0x1a45dfa3;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.SEGMENT_ID = 0x18538067;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.INFO_ID = 0x1549a966;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.TIMECODE_SCALE_ID = 0x2ad7b1;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.DURATION_ID = 0x4489;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.CUES_ID = 0x1c53bb6b;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.CUE_POINT_ID = 0xbb;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.CUE_TIME_ID = 0xb3;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.CUE_TRACK_POSITIONS_ID = 0xb7;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.CUE_CLUSTER_POSITION = 0xf1;
/**
* Parses SegmentReferences from a WebM container.
* @param {!ArrayBuffer} cuesData The WebM container's "Cueing Data" section.
* @param {!ArrayBuffer} initData The WebM container's headers.
* @param {!Array.<string>} uris The possible locations of the WebM file that
* contains the segments.
* @param {number} presentationTimeOffset
* @return {!Array.<!shaka.media.SegmentReference>}
* @throws {shaka.util.Error}
* @see http://www.matroska.org/technical/specs/index.html
* @see http://www.webmproject.org/docs/container/
*/
shaka.media.WebmSegmentIndexParser.prototype.parse = function(
cuesData, initData, uris, presentationTimeOffset) {
var tuple = this.parseWebmContainer_(initData);
var parser = new shaka.util.EbmlParser(new DataView(cuesData));
var cuesElement = parser.parseElement();
if (cuesElement.id != shaka.media.WebmSegmentIndexParser.CUES_ID) {
shaka.log.error('Not a Cues element.');
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_CUES_ELEMENT_MISSING);
}
return this.parseCues_(
cuesElement, tuple.segmentOffset, tuple.timecodeScale, tuple.duration,
uris, presentationTimeOffset);
};
/**
* Parses a WebM container to get the segment's offset, timecode scale, and
* duration.
*
* @param {!ArrayBuffer} initData
* @return {{segmentOffset: number, timecodeScale: number, duration: number}}
* The segment's offset in bytes, the segment's timecode scale in seconds,
* and the duration in seconds.
* @throws {shaka.util.Error}
* @private
*/
shaka.media.WebmSegmentIndexParser.prototype.parseWebmContainer_ = function(
initData) {
var parser = new shaka.util.EbmlParser(new DataView(initData));
// Check that the WebM container data starts with the EBML header, but
// skip its contents.
var ebmlElement = parser.parseElement();
if (ebmlElement.id != shaka.media.WebmSegmentIndexParser.EBML_ID) {
shaka.log.error('Not an EBML element.');
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_EBML_HEADER_ELEMENT_MISSING);
}
var segmentElement = parser.parseElement();
if (segmentElement.id != shaka.media.WebmSegmentIndexParser.SEGMENT_ID) {
shaka.log.error('Not a Segment element.');
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_SEGMENT_ELEMENT_MISSING);
}
// This value is used as the initial offset to the first referenced segment.
var segmentOffset = segmentElement.getOffset();
// Parse the Segment element to get the segment info.
var segmentInfo = this.parseSegment_(segmentElement);
return {
segmentOffset: segmentOffset,
timecodeScale: segmentInfo.timecodeScale,
duration: segmentInfo.duration
};
};
/**
* Parses a WebM Info element to get the segment's timecode scale and duration.
* @param {!shaka.util.EbmlElement} segmentElement
* @return {{timecodeScale: number, duration: number}} The segment's timecode
* scale in seconds and duration in seconds.
* @throws {shaka.util.Error}
* @private
*/
shaka.media.WebmSegmentIndexParser.prototype.parseSegment_ = function(
segmentElement) {
var parser = segmentElement.createParser();
// Find the Info element.
var infoElement = null;
while (parser.hasMoreData()) {
var elem = parser.parseElement();
if (elem.id != shaka.media.WebmSegmentIndexParser.INFO_ID) {
continue;
}
infoElement = elem;
break;
}
if (!infoElement) {
shaka.log.error('Not an Info element.');
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_INFO_ELEMENT_MISSING);
}
return this.parseInfo_(infoElement);
};
/**
* Parses a WebM Info element to get the segment's timecode scale and duration.
* @param {!shaka.util.EbmlElement} infoElement
* @return {{timecodeScale: number, duration: number}} The segment's timecode
* scale in seconds and duration in seconds.
* @throws {shaka.util.Error}
* @private
*/
shaka.media.WebmSegmentIndexParser.prototype.parseInfo_ = function(
infoElement) {
var parser = infoElement.createParser();
// The timecode scale factor in units of [nanoseconds / T], where [T] are the
// units used to express all other time values in the WebM container. By
// default it's assumed that [T] == [milliseconds].
var timecodeScaleNanoseconds = 1000000;
/** @type {?number} */
var durationScale = null;
while (parser.hasMoreData()) {
var elem = parser.parseElement();
if (elem.id == shaka.media.WebmSegmentIndexParser.TIMECODE_SCALE_ID) {
timecodeScaleNanoseconds = elem.getUint();
} else if (elem.id == shaka.media.WebmSegmentIndexParser.DURATION_ID) {
durationScale = elem.getFloat();
}
}
if (durationScale == null) {
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_DURATION_ELEMENT_MISSING);
}
// The timecode scale factor in units of [seconds / T].
var timecodeScale = timecodeScaleNanoseconds / 1000000000;
// The duration is stored in units of [T]
var durationSeconds = durationScale * timecodeScale;
return {timecodeScale: timecodeScale, duration: durationSeconds};
};
/**
* Parses a WebM CuesElement.
* @param {!shaka.util.EbmlElement} cuesElement
* @param {number} segmentOffset
* @param {number} timecodeScale
* @param {number} duration
* @param {!Array.<string>} uris
* @param {number} presentationTimeOffset
* @return {!Array.<!shaka.media.SegmentReference>}
* @throws {shaka.util.Error}
* @private
*/
shaka.media.WebmSegmentIndexParser.prototype.parseCues_ = function(
cuesElement, segmentOffset, timecodeScale, duration, uris,
presentationTimeOffset) {
var references = [];
var getUris = function() { return uris; };
var parser = cuesElement.createParser();
var lastTime = -1;
var lastOffset = -1;
while (parser.hasMoreData()) {
var elem = parser.parseElement();
if (elem.id != shaka.media.WebmSegmentIndexParser.CUE_POINT_ID) {
continue;
}
var tuple = this.parseCuePoint_(elem);
if (!tuple) {
continue;
}
// Substract presentationTimeOffset from unscalled time
var currentTime = timecodeScale *
(tuple.unscaledTime - presentationTimeOffset);
var currentOffset = segmentOffset + tuple.relativeOffset;
if (lastTime >= 0) {
goog.asserts.assert(lastOffset >= 0, 'last offset cannot be 0');
references.push(
new shaka.media.SegmentReference(
references.length,
lastTime, currentTime,
getUris,
lastOffset, currentOffset - 1));
}
lastTime = currentTime;
lastOffset = currentOffset;
}
if (lastTime >= 0) {
goog.asserts.assert(lastOffset >= 0, 'last offset cannot be 0');
references.push(
new shaka.media.SegmentReference(
references.length, lastTime, duration, getUris, lastOffset, null));
}
return references;
};
/**
* Parses a WebM CuePointElement to get an "unadjusted" segment reference.
* @param {shaka.util.EbmlElement} cuePointElement
* @return {{unscaledTime: number, relativeOffset: number}} The referenced
* segment's start time in units of [T] (see parseInfo_()), and the
* referenced segment's offset in bytes, relative to a WebM Segment
* element.
* @throws {shaka.util.Error}
* @private
*/
shaka.media.WebmSegmentIndexParser.prototype.parseCuePoint_ = function(
cuePointElement) {
var parser = cuePointElement.createParser();
// Parse CueTime element.
var cueTimeElement = parser.parseElement();
if (cueTimeElement.id != shaka.media.WebmSegmentIndexParser.CUE_TIME_ID) {
shaka.log.warning('Not a CueTime element.');
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_CUE_TIME_ELEMENT_MISSING);
}
var unscaledTime = cueTimeElement.getUint();
// Parse CueTrackPositions element.
var cueTrackPositionsElement = parser.parseElement();
if (cueTrackPositionsElement.id !=
shaka.media.WebmSegmentIndexParser.CUE_TRACK_POSITIONS_ID) {
shaka.log.warning('Not a CueTrackPositions element.');
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_CUE_TRACK_POSITIONS_ELEMENT_MISSING);
}
var cueTrackParser = cueTrackPositionsElement.createParser();
var relativeOffset = 0;
while (cueTrackParser.hasMoreData()) {
var elem = cueTrackParser.parseElement();
if (elem.id != shaka.media.WebmSegmentIndexParser.CUE_CLUSTER_POSITION) {
continue;
}
relativeOffset = elem.getUint();
break;
}
return { unscaledTime: unscaledTime, relativeOffset: relativeOffset };
};
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| data_uri_plugin.js | 3.03% | (1 / 33) | 0% | (0 / 12) | 0% | (0 / 2) | 3.03% | (1 / 33) | |
| http_plugin.js | 2.56% | (1 / 39) | 0% | (0 / 7) | 0% | (0 / 6) | 2.56% | (1 / 39) | |
| networking_engine.js | 0.89% | (1 / 112) | 0% | (0 / 36) | 0% | (0 / 20) | 0.89% | (1 / 112) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.net.DataUriPlugin');
goog.require('shaka.log');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.Error');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.Uint8ArrayUtils');
/**
* @namespace
* @summary A networking plugin to handle data URIs.
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs
* @param {string} uri
* @param {shakaExtern.Request} request
* @return {!Promise.<shakaExtern.Response>}
* @export
*/
shaka.net.DataUriPlugin = function(uri, request) {
return new Promise(function(resolve, reject) {
// Extract the scheme.
var parts = uri.split(':');
if (parts.length < 2 || parts[0] != 'data') {
shaka.log.error('Bad data URI, failed to parse scheme');
throw new shaka.util.Error(
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.MALFORMED_DATA_URI,
uri);
}
var path = parts.slice(1).join(':');
// Extract the encoding and MIME type (required but can be empty).
var infoAndData = path.split(',');
if (infoAndData.length < 2) {
shaka.log.error('Bad data URI, failed to extract encoding and MIME type');
throw new shaka.util.Error(
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.MALFORMED_DATA_URI,
uri);
}
var info = infoAndData[0];
var dataStr = window.decodeURIComponent(infoAndData.slice(1).join(','));
// Extract the encoding (optional).
var typeAndEncoding = info.split(';');
var encoding = null;
if (typeAndEncoding.length > 1)
encoding = typeAndEncoding[1];
// Convert the data.
/** @type {ArrayBuffer} */
var data;
if (encoding == 'base64') {
data = shaka.util.Uint8ArrayUtils.fromBase64(dataStr).buffer;
} else if (encoding) {
shaka.log.error('Bad data URI, unknown encoding');
throw new shaka.util.Error(
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.UNKNOWN_DATA_URI_ENCODING,
uri);
} else {
data = shaka.util.StringUtils.toUTF8(dataStr);
}
/** @type {shakaExtern.Response} */
var response = {
uri: uri,
data: data,
headers: {
'content-type': typeAndEncoding[0]
}
};
resolve(response);
});
};
shaka.net.NetworkingEngine.registerScheme('data', shaka.net.DataUriPlugin);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.net.HttpPlugin');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.Error');
goog.require('shaka.util.StringUtils');
/**
* @namespace
* @summary A networking plugin to handle http and https URIs via XHR.
* @param {string} uri
* @param {shakaExtern.Request} request
* @return {!Promise.<shakaExtern.Response>}
* @export
*/
shaka.net.HttpPlugin = function(uri, request) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(request.method, uri, true);
xhr.responseType = 'arraybuffer';
xhr.timeout = request.retryParameters.timeout;
xhr.withCredentials = request.allowCrossSiteCredentials;
xhr.onload = function(event) {
var target = event.target;
goog.asserts.assert(target, 'XHR onload has no target!');
var headers = target.getAllResponseHeaders().split('\r\n').reduce(
function(all, part) {
var header = part.split(': ');
all[header[0].toLowerCase()] = header.slice(1).join(': ');
return all;
},
{});
if (target.status >= 200 && target.status <= 299 &&
target.status != 202) {
// Most 2xx HTTP codes are success cases.
if (target.responseURL) {
uri = target.responseURL;
}
/** @type {shakaExtern.Response} */
var response = {
uri: uri,
data: target.response,
headers: headers,
fromCache: !!headers['x-shaka-from-cache']
};
resolve(response);
} else {
var responseText = null;
try {
responseText = shaka.util.StringUtils.fromBytesAutoDetect(
target.response);
} catch (exception) {}
shaka.log.debug('HTTP error text:', responseText);
reject(new shaka.util.Error(
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.BAD_HTTP_STATUS,
uri,
target.status,
responseText,
headers));
}
};
xhr.onerror = function(event) {
reject(new shaka.util.Error(
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.HTTP_ERROR,
uri));
};
xhr.ontimeout = function(event) {
reject(new shaka.util.Error(
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.TIMEOUT,
uri));
};
for (var k in request.headers) {
xhr.setRequestHeader(k, request.headers[k]);
}
xhr.send(request.body);
});
};
shaka.net.NetworkingEngine.registerScheme('http', shaka.net.HttpPlugin);
shaka.net.NetworkingEngine.registerScheme('https', shaka.net.HttpPlugin);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.net.NetworkingEngine');
goog.require('goog.Uri');
goog.require('goog.asserts');
goog.require('shaka.util.Error');
goog.require('shaka.util.Functional');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.PublicPromise');
/**
* NetworkingEngine wraps all networking operations. This accepts plugins that
* handle the actual request. A plugin is registered using registerScheme.
* Each scheme has at most one plugin to handle the request.
*
* @param {function(number, number, number)=} opt_onSegmentDownloaded Called
* when a segment is downloaded. Passed the wall-clock time, in
* milliseconds, when the request began (before any outbound request
* filters); the wall-clock time, in milliseconds, when the response ended
* (after all retries and inbound response filters); and the total number
* of bytes transferred.
*
* @struct
* @constructor
* @implements {shaka.util.IDestroyable}
* @export
*/
shaka.net.NetworkingEngine = function(opt_onSegmentDownloaded) {
/** @private {boolean} */
this.destroyed_ = false;
/** @private {!Array.<!Promise>} */
this.requests_ = [];
/** @private {!Array.<shakaExtern.RequestFilter>} */
this.requestFilters_ = [];
/** @private {!Array.<shakaExtern.ResponseFilter>} */
this.responseFilters_ = [];
/** @private {?function(number, number, number)} */
this.onSegmentDownloaded_ = opt_onSegmentDownloaded || null;
};
/**
* Request types. Allows a filter to decide which requests to read/alter.
*
* @enum {number}
* @export
*/
shaka.net.NetworkingEngine.RequestType = {
'MANIFEST': 0,
'SEGMENT': 1,
'LICENSE': 2
};
/**
* Contains the scheme plugins.
*
* @private {!Object.<string, ?shakaExtern.SchemePlugin>}
*/
shaka.net.NetworkingEngine.schemes_ = {};
/**
* Registers a scheme plugin. This plugin will handle all requests with the
* given scheme. If a plugin with the same scheme already exists, it is
* replaced.
*
* @param {string} scheme
* @param {shakaExtern.SchemePlugin} plugin
* @export
*/
shaka.net.NetworkingEngine.registerScheme = function(scheme, plugin) {
shaka.net.NetworkingEngine.schemes_[scheme] = plugin;
};
/**
* Removes a scheme plugin.
*
* @param {string} scheme
* @export
*/
shaka.net.NetworkingEngine.unregisterScheme = function(scheme) {
delete shaka.net.NetworkingEngine.schemes_[scheme];
};
/**
* Registers a new request filter. All filters are applied in the order they
* are registered.
*
* @param {shakaExtern.RequestFilter} filter
* @export
*/
shaka.net.NetworkingEngine.prototype.registerRequestFilter = function(filter) {
this.requestFilters_.push(filter);
};
/**
* Removes a request filter.
*
* @param {shakaExtern.RequestFilter} filter
* @export
*/
shaka.net.NetworkingEngine.prototype.unregisterRequestFilter =
function(filter) {
var filters = this.requestFilters_;
var i = filters.indexOf(filter);
if (i >= 0) {
filters.splice(i, 1);
}
};
/**
* Clear all request filters.
*
* @export
*/
shaka.net.NetworkingEngine.prototype.clearAllRequestFilters = function() {
this.requestFilters_ = [];
};
/**
* Registers a new response filter. All filters are applied in the order they
* are registered.
*
* @param {shakaExtern.ResponseFilter} filter
* @export
*/
shaka.net.NetworkingEngine.prototype.registerResponseFilter = function(filter) {
this.responseFilters_.push(filter);
};
/**
* Removes a response filter.
*
* @param {shakaExtern.ResponseFilter} filter
* @export
*/
shaka.net.NetworkingEngine.prototype.unregisterResponseFilter =
function(filter) {
var filters = this.responseFilters_;
var i = filters.indexOf(filter);
if (i >= 0) {
filters.splice(i, 1);
}
};
/**
* Clear all response filters.
*
* @export
*/
shaka.net.NetworkingEngine.prototype.clearAllResponseFilters = function() {
this.responseFilters_ = [];
};
/**
* Gets a copy of the default retry parameters.
*
* @return {shakaExtern.RetryParameters}
*/
shaka.net.NetworkingEngine.defaultRetryParameters = function() {
// Use a function rather than a constant member so the calling code can
// modify the values without affecting other call results.
return {
maxAttempts: 2,
baseDelay: 1000,
backoffFactor: 2,
fuzzFactor: 0.5,
timeout: 0
};
};
/**
* Makes a simple network request for the given URIs.
*
* @param {!Array.<string>} uris
* @param {shakaExtern.RetryParameters} retryParams
* @return {shakaExtern.Request}
*/
shaka.net.NetworkingEngine.makeRequest = function(
uris, retryParams) {
return {
uris: uris,
method: 'GET',
body: null,
headers: {},
allowCrossSiteCredentials: false,
retryParameters: retryParams
};
};
/**
* @override
* @export
*/
shaka.net.NetworkingEngine.prototype.destroy = function() {
var Functional = shaka.util.Functional;
this.destroyed_ = true;
this.requestFilters_ = [];
this.responseFilters_ = [];
var cleanup = [];
for (var i = 0; i < this.requests_.length; ++i) {
cleanup.push(this.requests_[i].catch(Functional.noop));
}
return Promise.all(cleanup);
};
/**
* Makes a network request and returns the resulting data.
*
* @param {shaka.net.NetworkingEngine.RequestType} type
* @param {shakaExtern.Request} request
* @return {!Promise.<shakaExtern.Response>}
* @export
*/
shaka.net.NetworkingEngine.prototype.request = function(type, request) {
if (this.destroyed_)
return Promise.reject();
goog.asserts.assert(request.uris && request.uris.length,
'Request without URIs!');
var startTimeMs = Date.now();
// Send to the filter first, in-case they change the URI.
var requestFilters = this.requestFilters_;
for (var i = 0; i < requestFilters.length; i++) {
try {
requestFilters[i](type, request);
} catch (error) {
return Promise.reject(error);
}
}
var retry = request.retryParameters || {};
var maxAttempts = retry.maxAttempts || 1;
var backoffFactor = retry.backoffFactor || 2.0;
var delay = (retry.baseDelay == null ? 1000 : retry.baseDelay);
var p = this.send_(type, request, 0);
for (var i = 1; i < maxAttempts; i++) {
var index = i % request.uris.length;
p = p.catch(this.resend_.bind(this, type, request, delay, index));
delay *= backoffFactor;
}
// Add the request to the array.
this.requests_.push(p);
return p.then(function(response) {
if (this.requests_.indexOf(p) >= 0) {
this.requests_.splice(this.requests_.indexOf(p), 1);
}
var endTimeMs = Date.now();
if (this.onSegmentDownloaded_ &&
type == shaka.net.NetworkingEngine.RequestType.SEGMENT) {
this.onSegmentDownloaded_(
startTimeMs, endTimeMs, response.data.byteLength);
}
return response;
}.bind(this)).catch(function(e) {
if (this.requests_.indexOf(p) >= 0) {
this.requests_.splice(this.requests_.indexOf(p), 1);
}
return Promise.reject(e);
}.bind(this));
};
/**
* Sends the given request to the correct plugin. This does not handle retry.
*
* @param {shaka.net.NetworkingEngine.RequestType} type
* @param {shakaExtern.Request} request
* @param {number} index
* @return {!Promise.<shakaExtern.Response>}
* @private
*/
shaka.net.NetworkingEngine.prototype.send_ = function(type, request, index) {
if (this.destroyed_)
return Promise.reject();
var uri = new goog.Uri(request.uris[index]);
var scheme = uri.getScheme();
if (!scheme) {
// If there is no scheme, infer one from the location.
scheme = shaka.net.NetworkingEngine.getLocationProtocol_();
goog.asserts.assert(scheme[scheme.length - 1] == ':',
'location.protocol expected to end with a colon!');
// Drop the colon.
scheme = scheme.slice(0, -1);
// Override the original URI to make the scheme explicit.
uri.setScheme(scheme);
request.uris[index] = uri.toString();
}
var plugin = shaka.net.NetworkingEngine.schemes_[scheme];
if (!plugin) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.UNSUPPORTED_SCHEME,
uri));
}
return plugin(request.uris[index], request).then(function(response) {
// Since we are inside a promise, no need to catch errors; they will result
// in a failed promise.
var responseFilters = this.responseFilters_;
for (var i = 0; i < responseFilters.length; i++) {
responseFilters[i](type, response);
}
return response;
}.bind(this));
};
/**
* Resends the request after applying a delay. This does not handle retry.
*
* @param {shaka.net.NetworkingEngine.RequestType} type
* @param {shakaExtern.Request} request
* @param {number} delayMs The current base delay.
* @param {number} index
* @return {!Promise.<shakaExtern.Response>}
* @private
*/
shaka.net.NetworkingEngine.prototype.resend_ =
function(type, request, delayMs, index) {
var p = new shaka.util.PublicPromise();
// Fuzz the delay to avoid tons of clients hitting the server at once
// after it recovers from whatever is causing it to fail.
var retry = request.retryParameters || {};
var fuzzFactor = (retry.fuzzFactor == null ? 0.5 : retry.fuzzFactor);
var negToPosOne = (Math.random() * 2.0) - 1.0;
var negToPosFuzzFactor = negToPosOne * fuzzFactor;
var fuzzedDelay = delayMs * (1.0 + negToPosFuzzFactor);
shaka.net.NetworkingEngine.setTimeout_(p.resolve, fuzzedDelay);
return p.then(this.send_.bind(this, type, request, index));
};
/**
* This is here only for testability. We can't mock location in our tests on
* all browsers, so instead we mock this.
*
* @return {string} The value of location.protocol.
* @private
*/
shaka.net.NetworkingEngine.getLocationProtocol_ = function() {
return location.protocol;
};
/**
* This is here only for testability. Mocking global setTimeout can lead to
* unintended interactions with other tests. So instead, we mock this.
*
* @param {Function} fn The callback to invoke when the timeout expires.
* @param {number} timeoutMs The timeout in milliseconds.
* @return {number} The timeout ID.
* @private
*/
shaka.net.NetworkingEngine.setTimeout_ = function(fn, timeoutMs) {
return window.setTimeout(fn, timeoutMs);
};
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| db_engine.js | 0.78% | (1 / 128) | 0% | (0 / 20) | 0% | (0 / 39) | 0.8% | (1 / 125) | |
| download_manager.js | 1.22% | (1 / 82) | 0% | (0 / 22) | 0% | (0 / 12) | 1.23% | (1 / 81) | |
| offline_manifest_parser.js | 2.13% | (1 / 47) | 0% | (0 / 12) | 0% | (0 / 16) | 2.27% | (1 / 44) | |
| offline_scheme.js | 4.35% | (1 / 23) | 0% | (0 / 6) | 0% | (0 / 4) | 4.35% | (1 / 23) | |
| offline_utils.js | 14.29% | (1 / 7) | 0% | (0 / 2) | 0% | (0 / 2) | 14.29% | (1 / 7) | |
| storage.js | 0.67% | (2 / 298) | 0% | (0 / 71) | 0% | (0 / 71) | 0.69% | (2 / 291) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.offline.DBEngine');
goog.require('goog.asserts');
goog.require('shaka.util.Error');
goog.require('shaka.util.Functional');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.PublicPromise');
/**
* This manages all operations on an IndexedDB. This wraps all operations
* in Promises. All Promises will resolve once the transaction has completed.
* Depending on the browser, this may or may not be after the data is flushed
* to disk. https://goo.gl/zMOeJc
*
* @struct
* @constructor
* @implements {shaka.util.IDestroyable}
*/
shaka.offline.DBEngine = function() {
/** @private {IDBDatabase} */
this.db_ = null;
/** @private {!Array.<shaka.offline.DBEngine.Operation>} */
this.operations_ = [];
/** @private {!Object.<string, number>} */
this.currentIdMap_ = {};
};
/**
* @typedef {{
* transaction: !IDBTransaction,
* promise: !shaka.util.PublicPromise
* }}
*
* @property {!IDBTransaction} transaction
* The transaction that this operation is using.
* @property {!shaka.util.PublicPromise} promise
* The promise associated with the operation.
*/
shaka.offline.DBEngine.Operation;
/** @private {string} */
shaka.offline.DBEngine.DB_NAME_ = 'shaka_offline_db';
/** @private @const {number} */
shaka.offline.DBEngine.DB_VERSION_ = 1;
/**
* Determines if the browsers supports IndexedDB.
* @return {boolean}
*/
shaka.offline.DBEngine.isSupported = function() {
return window.indexedDB != null;
};
/**
* Delete the database. There must be no open connections to the database.
* @return {!Promise}
*/
shaka.offline.DBEngine.deleteDatabase = function() {
if (!window.indexedDB)
return Promise.resolve();
var request =
window.indexedDB.deleteDatabase(shaka.offline.DBEngine.DB_NAME_);
var p = new shaka.util.PublicPromise();
request.onsuccess = function(event) {
goog.asserts.assert(event.newVersion == null, 'Unexpected database update');
p.resolve();
};
request.onerror = shaka.offline.DBEngine.onError_.bind(null, request, p);
return p;
};
/**
* Gets whether the DBEngine is initialized.
*
* @return {boolean}
*/
shaka.offline.DBEngine.prototype.initialized = function() {
return this.db_ != null;
};
/**
* Initializes the database and creates and required tables.
*
* @param {!Object.<string, string>} storeMap
* A map of store name to the key path.
* @return {!Promise}
*/
shaka.offline.DBEngine.prototype.init = function(storeMap) {
goog.asserts.assert(!this.db_, 'Already initialized');
var DBEngine = shaka.offline.DBEngine;
if (!DBEngine.isSupported()) {
return Promise.reject(
new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.INDEXED_DB_NOT_SUPPORTED));
}
var indexedDB = window.indexedDB;
var request = indexedDB.open(DBEngine.DB_NAME_, DBEngine.DB_VERSION_);
var promise = new shaka.util.PublicPromise();
request.onupgradeneeded = function(event) {
var db = event.target.result;
goog.asserts.assert(event.oldVersion == 0,
'Must be upgrading from version 0');
goog.asserts.assert(db.objectStoreNames.length == 0,
'Version 0 database should be empty');
for (var name in storeMap) {
db.createObjectStore(name, {keyPath: storeMap[name]});
}
};
request.onsuccess = (function(event) {
this.db_ = event.target.result;
promise.resolve();
}.bind(this));
request.onerror = DBEngine.onError_.bind(null, request, promise);
return promise.then(function() {
// For each store, get the next ID and store in the map.
var stores = Object.keys(storeMap);
return Promise.all(stores.map(function(store) {
return this.getNextId_(store).then(function(id) {
this.currentIdMap_[store] = id;
}.bind(this));
}.bind(this)));
}.bind(this));
};
/** @override */
shaka.offline.DBEngine.prototype.destroy = function() {
return Promise.all(this.operations_.map(function(op) {
try {
// If the transaction is considered finished but has not called the
// callbacks yet, it will still be in the list and this call will fail.
// Simply ignore errors.
op.transaction.abort();
} catch (e) {}
var Functional = shaka.util.Functional;
return op.promise.catch(Functional.noop);
})).then(function() {
if (this.db_) {
this.db_.close();
this.db_ = null;
}
}.bind(this));
};
/**
* Gets the item with the given ID in the store.
*
* @param {string} storeName
* @param {number} key
* @return {!Promise.<T>}
* @template T
*/
shaka.offline.DBEngine.prototype.get = function(storeName, key) {
return this.createOperation_(storeName, 'readonly', function(store) {
return store.get(key);
});
};
/**
* Calls the given callback for each value in the store. The promise will
* resolve after all items have been traversed.
*
* @param {string} storeName
* @param {function(T)} callback
* @return {!Promise}
* @template T
*/
shaka.offline.DBEngine.prototype.forEach = function(storeName, callback) {
return this.createOperation_(storeName, 'readonly', function(store) {
return store.openCursor();
}, function(/** IDBCursor */ cursor) {
if (!cursor) return;
callback(cursor.value);
cursor.continue();
});
};
/**
* Adds or updates the given value in the store.
*
* @param {string} storeName
* @param {!Object} value
* @return {!Promise}
*/
shaka.offline.DBEngine.prototype.insert = function(storeName, value) {
return this.createOperation_(storeName, 'readwrite', function(store) {
return store.put(value);
});
};
/**
* Removes the item with the given key.
*
* @param {string} storeName
* @param {number} key
* @return {!Promise}
*/
shaka.offline.DBEngine.prototype.remove = function(storeName, key) {
return this.createOperation_(storeName, 'readwrite', function(store) {
return store.delete(key);
});
};
/**
* Removes all items for which the given predicate returns true.
*
* @param {string} storeName
* @param {function(T):boolean} callback
* @return {!Promise.<number>}
* @template T
*/
shaka.offline.DBEngine.prototype.removeWhere = function(storeName, callback) {
var async = [];
return this.createOperation_(storeName, 'readwrite', function(store) {
return store.openCursor();
}, function(/** IDBCursor */ cursor) {
if (!cursor) return;
if (callback(cursor.value)) {
var request = cursor.delete();
var p = new shaka.util.PublicPromise();
request.onsuccess = p.resolve;
request.onerror = shaka.offline.DBEngine.onError_.bind(null, request, p);
async.push(p);
}
cursor.continue();
}).then(function() {
return Promise.all(async);
}).then(function() {
return async.length;
});
};
/**
* Reserves the next ID and returns it.
*
* @param {string} storeName
* @return {number}
*/
shaka.offline.DBEngine.prototype.reserveId = function(storeName) {
goog.asserts.assert(storeName in this.currentIdMap_,
'Store name must be passed to init()');
return this.currentIdMap_[storeName]++;
};
/**
* Gets the ID to start at.
*
* @param {string} storeName
* @return {!Promise.<number>}
* @private
*/
shaka.offline.DBEngine.prototype.getNextId_ = function(storeName) {
var ret = 0;
return this.createOperation_(storeName, 'readonly', function(store) {
return store.openCursor(null, 'prev');
}, function(/** IDBCursor */ cursor) {
if (cursor)
ret = cursor.key + 1;
}).then(function() { return ret; });
};
/**
* Creates a new transaction for the given store name and calls the given
* callback to create a request. It then wraps the given request in an
* operation and returns the resulting promise. The Promise resolves when
* the transaction is complete, which will be after opt_success is called.
*
* @param {string} storeName
* @param {string} type
* @param {function(!IDBObjectStore):!IDBRequest} createRequest
* @param {(function(*))=} opt_success The value of onsuccess for the request.
* @return {!Promise}
* @private
*/
shaka.offline.DBEngine.prototype.createOperation_ = function(
storeName, type, createRequest, opt_success) {
goog.asserts.assert(this.db_, 'Must not be destroyed');
goog.asserts.assert(type == 'readonly' || type == 'readwrite',
'Type must be "readonly" or "readwrite"');
var trans = this.db_.transaction([storeName], type);
var request = createRequest(trans.objectStore(storeName));
var p = new shaka.util.PublicPromise();
if (opt_success)
request.onsuccess = function(event) { opt_success(event.target.result); };
request.onerror = shaka.offline.DBEngine.onError_.bind(null, request, p);
var op = {transaction: trans, promise: p};
this.operations_.push(op);
// Only remove the transaction once it has completed, which may be after the
// request is complete (e.g. it may need to write to disk).
var removeOp = (function() {
var i = this.operations_.indexOf(op);
goog.asserts.assert(i >= 0, 'Operation must be in the list.');
this.operations_.splice(i, 1);
}.bind(this));
trans.oncomplete = function(event) {
removeOp();
p.resolve(request.result);
};
trans.onerror = function(event) {
removeOp();
shaka.offline.DBEngine.onError_(request, p, event);
};
return p;
};
/**
* Rejects the given Promise with an unknown error.
*
* @param {!IDBRequest} request
* @param {!shaka.util.PublicPromise} promise
* @param {Event} event
* @private
*/
shaka.offline.DBEngine.onError_ = function(request, promise, event) {
if (request.error.name == 'AbortError') {
promise.reject(new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.OPERATION_ABORTED));
} else {
promise.reject(new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.INDEXED_DB_ERROR, request.error));
}
// Firefox will raise an error which will cause a karma failure.
event.preventDefault();
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.offline.DownloadManager');
goog.require('goog.asserts');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.offline.OfflineUtils');
goog.require('shaka.util.Error');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.MapUtils');
/**
* This manages downloading segments and notifying the app of progress.
*
* @param {!shaka.net.NetworkingEngine} netEngine
* @param {shakaExtern.RetryParameters} retryParams
* @param {shakaExtern.OfflineConfiguration} config
*
* @struct
* @constructor
* @implements {shaka.util.IDestroyable}
*/
shaka.offline.DownloadManager = function(netEngine, retryParams, config) {
/**
* @private {!Object.<
* string, !Array.<shaka.offline.DownloadManager.Segment>>}
*/
this.segments_ = {};
/** @private {?shakaExtern.OfflineConfiguration} */
this.config_ = config;
/** @private {shaka.net.NetworkingEngine} */
this.netEngine_ = netEngine;
/** @private {?shakaExtern.RetryParameters} */
this.retryParams_ = retryParams;
/** @private {?shakaExtern.ManifestDB} */
this.manifest_ = null;
/** @private {Promise} */
this.promise_ = null;
/**
* The total number of bytes for segments that include a byte range.
* @private {number}
*/
this.givenBytesTotal_ = 0;
/**
* The number of bytes downloaded for segments that include a byte range.
* @private {number}
*/
this.givenBytesDownloaded_ = 0;
/**
* The total number of bytes estimated based on bandwidth for segments that
* do not include a byte range.
* @private {number}
*/
this.bandwidthBytesTotal_ = 0;
/**
* The estimated number of bytes downloaded for segments that do not have
* a byte range.
* @private {number}
*/
this.bandwidthBytesDownloaded_ = 0;
};
/**
* @typedef {{
* uris: !Array.<string>,
* startByte: number,
* endByte: ?number,
* bandwidthSize: number,
* callback: function(!ArrayBuffer):!Promise
* }}
*
* @property {!Array.<string>} uris
* The URIs to download the segment.
* @property {number} startByte
* The byte index the segment starts at.
* @property {?number} endByte
* The byte index the segment ends at, if present.
* @property {number} bandwidthSize
* The size of the segment as estimated by the bandwidth and segment duration.
* @property {function(!ArrayBuffer):!Promise} callback
* The callback to call once the segment is downloaded.
*/
shaka.offline.DownloadManager.Segment;
/** @override */
shaka.offline.DownloadManager.prototype.destroy = function() {
var ret = this.promise_ || Promise.resolve();
this.segments_ = {};
this.config_ = null;
this.netEngine_ = null;
this.retryParams_ = null;
this.manifest_ = null;
this.promise_ = null;
return ret;
};
/**
* Adds a segment to the list to be downloaded.
*
* @param {string} type
* @param {!shaka.media.SegmentReference|!shaka.media.InitSegmentReference} ref
* @param {number} bandwidthSize
* @param {function(!ArrayBuffer):!Promise} callback
*/
shaka.offline.DownloadManager.prototype.addSegment = function(
type, ref, bandwidthSize, callback) {
this.segments_[type] = this.segments_[type] || [];
this.segments_[type].push({
uris: ref.getUris(),
startByte: ref.startByte,
endByte: ref.endByte,
bandwidthSize: bandwidthSize,
callback: callback
});
};
/**
* Downloads all the segments.
*
* @param {shakaExtern.ManifestDB} manifest
* @return {!Promise}
*/
shaka.offline.DownloadManager.prototype.download = function(manifest) {
var MapUtils = shaka.util.MapUtils;
// Calculate progress estimates.
this.givenBytesTotal_ = 0;
this.givenBytesDownloaded_ = 0;
this.bandwidthBytesTotal_ = 0;
this.bandwidthBytesDownloaded_ = 0;
MapUtils.values(this.segments_).forEach(function(segments) {
segments.forEach(function(segment) {
if (segment.endByte != null)
this.givenBytesTotal_ += (segment.endByte - segment.startByte + 1);
else
this.bandwidthBytesTotal_ += segment.bandwidthSize;
}.bind(this));
}.bind(this));
this.manifest_ = manifest;
// Will be updated as we download for segments without a byte-range.
this.manifest_.size = this.givenBytesTotal_;
// Create separate download chains for different content types. This will
// allow audio and video to be downloaded in parallel.
var async = MapUtils.values(this.segments_).map(function(segments) {
var i = 0;
var downloadNext = (function() {
if (!this.config_) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.OPERATION_ABORTED));
}
if (i >= segments.length) return Promise.resolve();
var segment = segments[i++];
return this.downloadSegment_(segment).then(downloadNext);
}.bind(this));
return downloadNext();
}.bind(this));
this.segments_ = {};
return (this.promise_ = Promise.all(async));
};
/**
* Downloads the given segment and calls the callback.
*
* @param {shaka.offline.DownloadManager.Segment} segment
* @return {!Promise}
* @private
*/
shaka.offline.DownloadManager.prototype.downloadSegment_ = function(segment) {
goog.asserts.assert(this.retryParams_, 'Must not be destroyed');
var type = shaka.net.NetworkingEngine.RequestType.SEGMENT;
var request =
shaka.net.NetworkingEngine.makeRequest(segment.uris, this.retryParams_);
if (segment.startByte != 0 || segment.endByte != null) {
var end = segment.endByte == null ? '' : segment.endByte;
request.headers['Range'] = 'bytes=' + segment.startByte + '-' + end;
}
var byteCount;
return this.netEngine_.request(type, request)
.then(function(response) {
if (!this.manifest_) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.OPERATION_ABORTED));
}
byteCount = response.data.byteLength;
return segment.callback(response.data);
}.bind(this))
.then(function() {
if (!this.manifest_) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.OPERATION_ABORTED));
}
if (segment.endByte == null) {
// We didn't know the size, so it was an estimate.
this.manifest_.size += byteCount;
this.bandwidthBytesDownloaded_ += segment.bandwidthSize;
} else {
goog.asserts.assert(
byteCount == (segment.endByte - segment.startByte + 1),
'Incorrect download size');
this.givenBytesDownloaded_ += byteCount;
}
this.updateProgress_();
}.bind(this));
};
/**
* Calls the progress callback.
* @private
*/
shaka.offline.DownloadManager.prototype.updateProgress_ = function() {
var progress = (this.givenBytesDownloaded_ + this.bandwidthBytesDownloaded_) /
(this.givenBytesTotal_ + this.bandwidthBytesTotal_);
goog.asserts.assert(this.manifest_, 'Must not be destroyed');
var manifest = shaka.offline.OfflineUtils.getStoredContent(this.manifest_);
this.config_.progressCallback(manifest, progress);
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.offline.OfflineManifestParser');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.ManifestParser');
goog.require('shaka.media.PresentationTimeline');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.offline.DBEngine');
goog.require('shaka.offline.OfflineUtils');
goog.require('shaka.util.Error');
/**
* Creates a new offline manifest parser.
* @struct
* @constructor
* @implements {shakaExtern.ManifestParser}
*/
shaka.offline.OfflineManifestParser = function() {
};
/** @override */
shaka.offline.OfflineManifestParser.prototype.configure = function(config) {
// No-op
};
/** @override */
shaka.offline.OfflineManifestParser.prototype.start =
function(uri, networkingEngine, filterPeriod, onError) {
var parts = /^offline:([0-9]+)$/.exec(uri);
if (!parts) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.MALFORMED_OFFLINE_URI, uri));
}
var manifestId = Number(parts[1]);
var dbEngine = new shaka.offline.DBEngine();
return dbEngine.init(shaka.offline.OfflineUtils.DB_SCHEME)
.then(function() { return dbEngine.get('manifest', manifestId); })
.then(function(manifest) {
if (!manifest) {
throw new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.REQUESTED_ITEM_NOT_FOUND, manifestId);
}
var OfflineManifestParser = shaka.offline.OfflineManifestParser;
return OfflineManifestParser.reconstructManifest(manifest);
})
.then(
function(ret) {
return dbEngine.destroy().then(function() { return ret; });
},
function(err) {
return dbEngine.destroy().then(function() { throw err; });
});
};
/** @override */
shaka.offline.OfflineManifestParser.prototype.stop = function() {
return Promise.resolve();
};
/**
* Reconstructs a manifest object from the given database manifest.
*
* @param {shakaExtern.ManifestDB} manifest
* @return {shakaExtern.Manifest}
*/
shaka.offline.OfflineManifestParser.reconstructManifest = function(
manifest) {
var timeline = new shaka.media.PresentationTimeline(null, 0);
timeline.setDuration(manifest.duration);
var drmInfos = manifest.drmInfo ? [manifest.drmInfo] : [];
return {
presentationTimeline: timeline,
minBufferTime: 10,
offlineSessionIds: manifest.sessionIds,
periods: manifest.periods.map(function(period) {
return {
startTime: period.startTime,
streamSets: period.streams.map(function(streamDb) {
var refs = streamDb.segments.map(function(segment, i) {
var getUris = function() { return [segment.uri]; };
return new shaka.media.SegmentReference(
i, segment.startTime, segment.endTime, getUris, 0, null);
});
timeline.notifySegments(period.startTime, refs);
var segmentIndex = new shaka.media.SegmentIndex(refs);
var initRef = streamDb.initSegmentUri ?
new shaka.media.InitSegmentReference(
function() { return [streamDb.initSegmentUri]; }, 0, null) :
null;
var stream = {
id: streamDb.id,
createSegmentIndex: Promise.resolve.bind(Promise),
findSegmentPosition: segmentIndex.find.bind(segmentIndex),
getSegmentReference: segmentIndex.get.bind(segmentIndex),
initSegmentReference: initRef,
presentationTimeOffset: streamDb.presentationTimeOffset,
mimeType: streamDb.mimeType,
codecs: streamDb.codecs,
bandwidth: 0,
width: streamDb.width || undefined,
height: streamDb.height || undefined,
kind: streamDb.kind,
encrypted: streamDb.encrypted,
keyId: streamDb.keyId,
allowedByApplication: true,
allowedByKeySystem: true
};
var streamSet = {
language: streamDb.language,
type: streamDb.contentType,
primary: streamDb.primary,
drmInfos: drmInfos,
streams: [stream]
};
return streamSet;
})
};
})
};
};
shaka.media.ManifestParser.registerParserByMime(
'application/x-offline-manifest', shaka.offline.OfflineManifestParser);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.offline.OfflineScheme');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.offline.DBEngine');
goog.require('shaka.offline.OfflineUtils');
goog.require('shaka.util.Error');
/**
* @namespace
* @summary A plugin that handles requests for offline content.
* @param {string} uri
* @param {shakaExtern.Request} request
* @return {!Promise.<shakaExtern.Response>}
* @export
*/
shaka.offline.OfflineScheme = function(uri, request) {
var manifestParts = /^offline:([0-9]+)$/.exec(uri);
if (manifestParts) {
/** @type {shakaExtern.Response} */
var response = {
uri: uri,
data: new ArrayBuffer(0),
headers: {'content-type': 'application/x-offline-manifest'}
};
return Promise.resolve(response);
}
var segmentParts = /^offline:[0-9]+\/[0-9]+\/([0-9]+)$/.exec(uri);
if (segmentParts) {
var segmentId = Number(segmentParts[1]);
var scheme = shaka.offline.OfflineUtils.DB_SCHEME;
var dbEngine = new shaka.offline.DBEngine();
return dbEngine.init(scheme)
.then(function() { return dbEngine.get('segment', segmentId); })
.then(function(segment) {
return dbEngine.destroy().then(function() {
if (!segment) {
throw new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.REQUESTED_ITEM_NOT_FOUND, segmentId);
}
return {uri: uri, data: segment.data, headers: {}};
});
});
}
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.MALFORMED_OFFLINE_URI, uri));
};
shaka.net.NetworkingEngine.registerScheme(
'offline', shaka.offline.OfflineScheme);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.offline.OfflineUtils');
goog.require('goog.asserts');
/** @const {!Object.<string, string>} */
shaka.offline.OfflineUtils.DB_SCHEME = {'manifest': 'key', 'segment': 'key'};
/**
* Converts the given database manifest to a storedContent structure.
*
* @param {shakaExtern.ManifestDB} manifest
* @return {shakaExtern.StoredContent}
*/
shaka.offline.OfflineUtils.getStoredContent = function(manifest) {
goog.asserts.assert(manifest.periods.length > 0,
'Must be at least one Period.');
return {
offlineUri: 'offline:' + manifest.key,
originalManifestUri: manifest.originalManifestUri,
duration: manifest.duration,
size: manifest.size,
tracks: manifest.periods[0].streams.map(function(stream) {
return {
id: stream.id,
active: false,
type: stream.contentType,
bandwidth: 0,
language: stream.language,
kind: stream.kind || null,
width: stream.width,
height: stream.height,
frameRate: stream.frameRate,
codecs: stream.codecs
};
}),
appMetadata: manifest.appMetadata
};
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 | 2 1 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.offline.Storage');
goog.require('goog.asserts');
goog.require('shaka.Player');
goog.require('shaka.log');
goog.require('shaka.media.DrmEngine');
goog.require('shaka.media.ManifestParser');
goog.require('shaka.offline.DBEngine');
goog.require('shaka.offline.DownloadManager');
goog.require('shaka.offline.OfflineManifestParser');
goog.require('shaka.offline.OfflineUtils');
goog.require('shaka.util.ConfigUtils');
goog.require('shaka.util.Error');
goog.require('shaka.util.Functional');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.LanguageUtils');
goog.require('shaka.util.StreamUtils');
/**
* This manages persistent offline data including storage, listing, and deleting
* stored manifests. Playback of offline manifests are done using Player
* using the special URI (e.g. 'offline:12').
*
* First, check support() to see if offline is supported by the platform.
* Second, configure() the storage object with callbacks to your application.
* Third, call store(), remove(), or list() as needed.
* When done, call destroy().
*
* @param {shaka.Player} player
* The player instance to pull configuration data from.
*
* @struct
* @constructor
* @implements {shaka.util.IDestroyable}
* @export
*/
shaka.offline.Storage = function(player) {
/** @private {shaka.offline.DBEngine} */
this.dbEngine_ = new shaka.offline.DBEngine();
/** @private {shaka.Player} */
this.player_ = player;
/** @private {?shakaExtern.OfflineConfiguration} */
this.config_ = this.defaultConfig_();
/** @private {shaka.media.DrmEngine} */
this.drmEngine_ = null;
/** @private {boolean} */
this.storeInProgress_ = false;
/** @private {Array.<shakaExtern.Track>} */
this.firstPeriodTracks_ = null;
/**
* The IDs of the segments that have been stored for an in-progress store().
* This is used to cleanup in destroy().
* @private {!Array.<number>}
*/
this.inProgressSegmentIds_ = [];
/** @private {number} */
this.manifestId_ = -1;
/** @private {number} */
this.duration_ = 0;
/** @private {?shakaExtern.Manifest} */
this.manifest_ = null;
var netEngine = player.getNetworkingEngine();
goog.asserts.assert(netEngine, 'Player must not be destroyed');
/** @private {shaka.offline.DownloadManager} */
this.downloadManager_ = new shaka.offline.DownloadManager(
netEngine, player.getConfiguration().streaming.retryParameters,
this.config_);
};
/**
* Gets whether offline storage is supported. Returns true if offline storage
* is supported for clear content. Support for offline storage of encrypted
* content will not be determined until storage is attempted.
*
* @return {boolean}
* @export
*/
shaka.offline.Storage.support = function() {
return shaka.offline.DBEngine.isSupported();
};
/**
* Sets the DBEngine instance to use. This is used for testing.
*
* @param {!shaka.offline.DBEngine} engine
*/
shaka.offline.Storage.prototype.setDbEngine = function(engine) {
goog.asserts.assert(!this.dbEngine_.initialized(),
'Should not be initialized yet');
this.dbEngine_ = engine;
};
/**
* @override
* @export
*/
shaka.offline.Storage.prototype.destroy = function() {
var segments = this.inProgressSegmentIds_;
var dbEngine = this.dbEngine_;
// Destroy the download manager first to ensure segments are not added while
// we delete old ones.
var ret = !this.downloadManager_ ?
Promise.resolve() :
this.downloadManager_.destroy()
.catch(function() {})
.then(function() {
return Promise.all(segments.map(function(id) {
return dbEngine.remove('segment', id);
}));
})
.then(function() { return dbEngine.destroy(); });
this.dbEngine_ = null;
this.downloadManager_ = null;
this.player_ = null;
this.config_ = null;
return ret;
};
/**
* Sets configuration values for Storage. This is not associated with
* Player.configure and will not change Player.
*
* There are two important callbacks configured here: one for download progress,
* and one to decide which tracks to store.
*
* The default track selection callback will store the largest SD video track.
* Provide your own callback to choose the tracks you want to store.
*
* @param {shakaExtern.OfflineConfiguration} config
* @export
*/
shaka.offline.Storage.prototype.configure = function(config) {
goog.asserts.assert(this.config_, 'Must not be destroyed');
shaka.util.ConfigUtils.mergeConfigObjects(
this.config_, config, this.defaultConfig_(), {}, '');
};
/**
* Stores the given manifest. If the content is encrypted, and encrypted
* content cannot be stored on this platform, the Promise will be rejected with
* error code 6001, REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE.
*
* @param {string} manifestUri The URI of the manifest to store.
* @param {!Object} appMetadata An arbitrary object from the application that
* will be stored along-side the offline content. Use this for any
* application-specific metadata you need associated with the stored content.
* For details on the data types that can be stored here, please refer to
* https://goo.gl/h62coS
* @param {!shakaExtern.ManifestParser.Factory=} opt_manifestParserFactory
* @return {!Promise.<shakaExtern.StoredContent>} A Promise to a structure
* representing what was stored. The "offlineUri" member is the URI that
* should be given to Player.load() to play this piece of content offline.
* The "appMetadata" member is the appMetadata argument you passed to store().
* @export
*/
shaka.offline.Storage.prototype.store = function(
manifestUri, appMetadata, opt_manifestParserFactory) {
if (this.storeInProgress_) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.STORE_ALREADY_IN_PROGRESS));
}
this.storeInProgress_ = true;
/** @type {shakaExtern.ManifestDB} */
var manifestDb;
var error = null;
var onError = function(e) { error = e; };
return this.initIfNeeded_()
.then(function() {
this.checkDestroyed_();
return this.loadInternal(
manifestUri, onError, opt_manifestParserFactory);
}.bind(this)).then((
/**
* @param {{manifest: shakaExtern.Manifest,
* drmEngine: !shaka.media.DrmEngine}} data
* @return {!Promise}
*/
function(data) {
this.checkDestroyed_();
this.manifest_ = data.manifest;
this.drmEngine_ = data.drmEngine;
if (this.manifest_.presentationTimeline.isLive() ||
this.manifest_.presentationTimeline.isInProgress()) {
throw new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.CANNOT_STORE_LIVE_OFFLINE, manifestUri);
}
// Re-filter now that DrmEngine is initialized.
this.manifest_.periods.forEach(this.filterPeriod_.bind(this));
this.manifestId_ = this.dbEngine_.reserveId('manifest');
this.duration_ = 0;
manifestDb = this.createOfflineManifest_(manifestUri, appMetadata);
return this.downloadManager_.download(manifestDb);
})
.bind(this))
.then(function() {
this.checkDestroyed_();
// Throw any errors from the manifest parser or DrmEngine.
if (error)
throw error;
return this.dbEngine_.insert('manifest', manifestDb);
}.bind(this))
.then(function() {
return this.cleanup_();
}.bind(this))
.then(function() {
return shaka.offline.OfflineUtils.getStoredContent(manifestDb);
}.bind(this))
.catch(function(err) {
var Functional = shaka.util.Functional;
return this.cleanup_().catch(Functional.noop).then(function() {
throw err;
});
}.bind(this));
};
/**
* Removes the given stored content.
*
* @param {shakaExtern.StoredContent} content
* @return {!Promise}
* @export
*/
shaka.offline.Storage.prototype.remove = function(content) {
var uri = content.offlineUri;
var parts = /^offline:([0-9]+)$/.exec(uri);
if (!parts) {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.MALFORMED_OFFLINE_URI, uri));
}
var error = null;
var onError = function(e) {
// Ignore errors if the session was already removed.
if (e.code != shaka.util.Error.Code.OFFLINE_SESSION_REMOVED)
error = e;
};
/** @type {shakaExtern.ManifestDB} */
var manifestDb;
/** @type {!shaka.media.DrmEngine} */
var drmEngine;
var manifestId = Number(parts[1]);
return this.initIfNeeded_().then(function() {
this.checkDestroyed_();
return this.dbEngine_.get('manifest', manifestId);
}.bind(this)).then((
/**
* @param {?shakaExtern.ManifestDB} data
* @return {!Promise}
*/
function(data) {
this.checkDestroyed_();
if (!data) {
throw new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.REQUESTED_ITEM_NOT_FOUND, uri);
}
manifestDb = data;
var manifest =
shaka.offline.OfflineManifestParser.reconstructManifest(manifestDb);
var netEngine = this.player_.getNetworkingEngine();
goog.asserts.assert(netEngine, 'Player must not be destroyed');
drmEngine =
new shaka.media.DrmEngine(netEngine, onError, function() {});
drmEngine.configure(this.player_.getConfiguration().drm);
return drmEngine.init(manifest, true /* isOffline */);
})
.bind(this)).then(function() {
return drmEngine.removeSessions(manifestDb.sessionIds);
}.bind(this)).then(function() {
return drmEngine.destroy();
}.bind(this)).then(function() {
this.checkDestroyed_();
if (error) throw error;
var Functional = shaka.util.Functional;
// Get every segment for every stream in the manifest.
/** @type {!Array.<number>} */
var segments = manifestDb.periods.map(function(period) {
return period.streams.map(function(stream) {
var segments = stream.segments.map(function(segment) {
var parts = /^offline:[0-9]+\/[0-9]+\/([0-9]+)$/.exec(segment.uri);
goog.asserts.assert(parts, 'Invalid offline URI');
return Number(parts[1]);
});
if (stream.initSegmentUri) {
var parts = /^offline:[0-9]+\/[0-9]+\/([0-9]+)$/.exec(
stream.initSegmentUri);
goog.asserts.assert(parts, 'Invalid offline URI');
segments.push(Number(parts[1]));
}
return segments;
}).reduce(Functional.collapseArrays, []);
}).reduce(Functional.collapseArrays, []);
// Delete all the segments.
var deleteCount = 0;
var segmentCount = segments.length;
var callback = this.config_.progressCallback;
return this.dbEngine_.removeWhere('segment', function(segment) {
var i = segments.indexOf(segment.key);
if (i >= 0) {
callback(content, deleteCount / segmentCount);
deleteCount++;
}
return i >= 0;
}.bind(this));
}.bind(this)).then(function() {
this.checkDestroyed_();
this.config_.progressCallback(content, 1);
return this.dbEngine_.remove('manifest', manifestId);
}.bind(this));
};
/**
* Lists all the stored content available.
*
* @return {!Promise.<!Array.<shakaExtern.StoredContent>>} A Promise to an
* array of structures representing all stored content. The "offlineUri"
* member of the structure is the URI that should be given to Player.load()
* to play this piece of content offline. The "appMetadata" member is the
* appMetadata argument you passed to store().
* @export
*/
shaka.offline.Storage.prototype.list = function() {
/** @type {!Array.<shakaExtern.StoredContent>} */
var storedContents = [];
return this.initIfNeeded_()
.then(function() {
this.checkDestroyed_();
return this.dbEngine_.forEach(
'manifest', function(/** shakaExtern.ManifestDB */ manifest) {
storedContents.push(
shaka.offline.OfflineUtils.getStoredContent(manifest));
});
}.bind(this))
.then(function() { return storedContents; });
};
/**
* Loads the given manifest, parses it, and constructs the DrmEngine. This
* stops the manifest parser. This may be replaced by tests.
*
* @param {string} manifestUri
* @param {function(*)} onError
* @param {!shakaExtern.ManifestParser.Factory=} opt_manifestParserFactory
* @return {!Promise.<{
* manifest: shakaExtern.Manifest,
* drmEngine: !shaka.media.DrmEngine
* }>}
*/
shaka.offline.Storage.prototype.loadInternal = function(
manifestUri, onError, opt_manifestParserFactory) {
var netEngine = /** @type {!shaka.net.NetworkingEngine} */ (
this.player_.getNetworkingEngine());
var config = this.player_.getConfiguration();
/** @type {shakaExtern.Manifest} */
var manifest;
/** @type {!shaka.media.DrmEngine} */
var drmEngine;
/** @type {!shakaExtern.ManifestParser} */
var manifestParser;
var onKeyStatusChange = function() {};
return shaka.media.ManifestParser
.getFactory(
manifestUri, netEngine, config.manifest.retryParameters,
opt_manifestParserFactory)
.then(function(factory) {
this.checkDestroyed_();
manifestParser = new factory();
manifestParser.configure(config.manifest);
return manifestParser.start(
manifestUri, netEngine, this.filterPeriod_.bind(this), onError);
}.bind(this))
.then(function(data) {
this.checkDestroyed_();
manifest = data;
drmEngine =
new shaka.media.DrmEngine(netEngine, onError, onKeyStatusChange);
drmEngine.configure(config.drm);
return drmEngine.init(manifest, true /* isOffline */);
}.bind(this))
.then(function() {
this.checkDestroyed_();
return this.createSegmentIndex_(manifest);
}.bind(this))
.then(function() {
this.checkDestroyed_();
return drmEngine.createOrLoad();
}.bind(this))
.then(function() {
this.checkDestroyed_();
return manifestParser.stop();
}.bind(this))
.then(function() {
this.checkDestroyed_();
return {manifest: manifest, drmEngine: drmEngine};
}.bind(this))
.catch(function(error) {
if (manifestParser)
return manifestParser.stop().then(function() { throw error; });
else
throw error;
});
};
/**
* The default track selection function.
*
* @param {!Array.<shakaExtern.Track>} tracks
* @return {!Array.<shakaExtern.Track>}
* @private
*/
shaka.offline.Storage.prototype.defaultTrackSelect_ = function(tracks) {
var LanguageUtils = shaka.util.LanguageUtils;
var selectedTracks = [];
// Select the highest bandwidth video track with height <= 480.
var videoTracks = tracks.filter(function(t) {
return t.type == 'video' && t.height <= 480;
});
videoTracks.sort(function(a, b) { return b.bandwidth - a.bandwidth; });
if (videoTracks.length)
selectedTracks.push(videoTracks[0]);
// Select middle bandwidth audio track with best audio pref language match.
var audioLangPref = LanguageUtils.normalize(
this.player_.getConfiguration().preferredAudioLanguage);
var matchTypes = [
LanguageUtils.MatchType.EXACT,
LanguageUtils.MatchType.BASE_LANGUAGE_OKAY,
LanguageUtils.MatchType.OTHER_SUB_LANGUAGE_OKAY
];
var allAudioTracks =
tracks.filter(function(t) { return t.type == 'audio'; });
// For each match type, get the tracks that match the audio preference for
// that match type.
var tracksByMatchType = matchTypes.map(function(match) {
return allAudioTracks.filter(function(track) {
var lang = LanguageUtils.normalize(track.language);
return LanguageUtils.match(match, audioLangPref, lang);
});
});
// Find the best match type that has any matches, defaulting to all tracks.
var audioTracks = allAudioTracks;
for (var i = 0; i < tracksByMatchType.length; i++) {
if (tracksByMatchType[i].length) {
audioTracks = tracksByMatchType[i];
}
}
audioTracks.sort(function(a, b) { return a.bandwidth - b.bandwidth; });
if (audioTracks.length)
selectedTracks.push(audioTracks[Math.floor(audioTracks.length / 2)]);
// Select all text tracks with any text pref language match.
var textLangPref = LanguageUtils.normalize(
this.player_.getConfiguration().preferredTextLanguage);
var matchesTextPref = LanguageUtils.match.bind(
null, LanguageUtils.MatchType.OTHER_SUB_LANGUAGE_OKAY, textLangPref);
selectedTracks.push.apply(selectedTracks, tracks.filter(function(t) {
var language = LanguageUtils.normalize(t.language);
return t.type == 'text' && matchesTextPref(language);
}));
return selectedTracks;
};
/**
* @return {shakaExtern.OfflineConfiguration}
* @private
*/
shaka.offline.Storage.prototype.defaultConfig_ = function() {
return {
trackSelectionCallback: this.defaultTrackSelect_.bind(this),
progressCallback: function(storedContent, percent) {
// Reference arguments to keep closure from removing it.
// If the argument is removed, it breaks our function length check
// in mergeConfigObjects_().
// NOTE: Chrome App Content Security Policy prohibits usage of new
// Function().
if (storedContent || percent) return null;
}
};
};
/**
* Initializes the DBEngine if it is not already.
*
* @return {!Promise}
* @private
*/
shaka.offline.Storage.prototype.initIfNeeded_ = function() {
var scheme = shaka.offline.OfflineUtils.DB_SCHEME;
return this.dbEngine_.initialized() ? Promise.resolve() :
this.dbEngine_.init(scheme);
};
/**
* @param {shakaExtern.Period} period
* @private
*/
shaka.offline.Storage.prototype.filterPeriod_ = function(period) {
function getFirstStreamOfType(period, tracks, contentType) {
var tracksOfType =
tracks.filter(function(track) { return track.type == contentType; });
if (tracksOfType.length == 0)
return null;
var data =
shaka.util.StreamUtils.findStreamForTrack(period, tracksOfType[0]);
goog.asserts.assert(
data, 'Could not find stream with id ' + tracksOfType[0].id);
return data.stream;
}
var StreamUtils = shaka.util.StreamUtils;
var activeStreams = {};
if (this.firstPeriodTracks_) {
// Use the first stream of each content type as the "active stream". This
// is then used to filter out the streams that are not compatible with it.
// This ensures that in multi-Period content, all Periods have streams
// with compatible MIME types.
activeStreams = {
'video': getFirstStreamOfType(
this.manifest_.periods[0], this.firstPeriodTracks_, 'video'),
'audio': getFirstStreamOfType(
this.manifest_.periods[0], this.firstPeriodTracks_, 'audio')
};
}
StreamUtils.filterPeriod(this.drmEngine_, activeStreams, period);
StreamUtils.applyRestrictions(
period, this.player_.getConfiguration().restrictions,
/* maxHwRes */ { width: Infinity, height: Infinity });
};
/**
* Cleans up the current store and destroys any objects. This object is still
* usable after this.
*
* @return {!Promise}
* @private
*/
shaka.offline.Storage.prototype.cleanup_ = function() {
var ret = this.drmEngine_ ? this.drmEngine_.destroy() : Promise.resolve();
this.drmEngine_ = null;
this.manifest_ = null;
this.storeInProgress_ = false;
this.firstPeriodTracks_ = null;
this.inProgressSegmentIds_ = [];
this.manifestId_ = -1;
return ret;
};
/**
* Calls createSegmentIndex for all streams in the manifest.
*
* @param {shakaExtern.Manifest} manifest
* @return {!Promise}
* @private
*/
shaka.offline.Storage.prototype.createSegmentIndex_ = function(manifest) {
var Functional = shaka.util.Functional;
var streams = manifest.periods
.map(function(period) { return period.streamSets; })
.reduce(Functional.collapseArrays, [])
.map(function(streamSet) { return streamSet.streams; })
.reduce(Functional.collapseArrays, []);
return Promise.all(
streams.map(function(stream) { return stream.createSegmentIndex(); }));
};
/**
* Creates an offline 'manifest' for the real manifest. This does not store
* the segments yet, only adds them to the download manager through
* createPeriod_.
*
* @param {string} originalManifestUri
* @param {!Object} appMetadata
* @return {shakaExtern.ManifestDB}
* @private
*/
shaka.offline.Storage.prototype.createOfflineManifest_ = function(
originalManifestUri, appMetadata) {
var periods = this.manifest_.periods.map(this.createPeriod_.bind(this));
var drmInfo = this.drmEngine_.getDrmInfo();
var sessions = this.drmEngine_.getSessionIds();
if (drmInfo) {
if (!sessions.length) {
throw new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.NO_INIT_DATA_FOR_OFFLINE, originalManifestUri);
}
// Don't store init data since we have stored sessions.
drmInfo.initData = [];
}
return {
key: this.manifestId_,
originalManifestUri: originalManifestUri,
duration: this.duration_,
size: 0,
periods: periods,
sessionIds: sessions,
drmInfo: drmInfo,
appMetadata: appMetadata
};
};
/**
* Converts a manifest Period to a database Period. This will use the current
* configuration to get the tracks to use, then it will search each segment
* index and add all the segments to the download manager through createStream_.
*
* @param {shakaExtern.Period} period
* @return {shakaExtern.PeriodDB}
* @private
*/
shaka.offline.Storage.prototype.createPeriod_ = function(period) {
var allTracks = shaka.util.StreamUtils.getTracks(period, null);
var tracks = this.config_.trackSelectionCallback(allTracks);
if (this.firstPeriodTracks_ == null) {
this.firstPeriodTracks_ = tracks;
// Now that the first tracks are chosen, filter again. This ensures all
// Periods have compatible content types.
this.manifest_.periods.forEach(this.filterPeriod_.bind(this));
}
for (var i = tracks.length - 1; i > 0; --i) {
var found = false;
for (var j = i - 1; j >= 0; --j) {
if (tracks[i].type == tracks[j].type &&
tracks[i].kind == tracks[j].kind &&
tracks[i].language == tracks[j].language) {
shaka.log.warning(
'Multiple tracks of the same type/kind/language given.');
found = true;
break;
}
}
if (found) break;
}
var streams = tracks.map(function(track) {
var data = shaka.util.StreamUtils.findStreamForTrack(period, track);
goog.asserts.assert(data, 'Could not find track with id ' + track.id);
return this.createStream_(period, data.streamSet, data.stream);
}.bind(this));
return {
startTime: period.startTime,
streams: streams
};
};
/**
* Converts a manifest stream to a database stream. This will search the
* segment index and add all the segments to the download manager.
*
* @param {shakaExtern.Period} period
* @param {shakaExtern.StreamSet} streamSet
* @param {shakaExtern.Stream} stream
* @return {shakaExtern.StreamDB}
* @private
*/
shaka.offline.Storage.prototype.createStream_ = function(
period, streamSet, stream) {
/** @type {!Array.<shakaExtern.SegmentDB>} */
var segmentsDb = [];
var startTime = this.manifest_.presentationTimeline.getEarliestStart();
var endTime = startTime;
var i = stream.findSegmentPosition(startTime);
var ref = (i != null ? stream.getSegmentReference(i) : null);
while (ref) {
var id = this.dbEngine_.reserveId('segment');
var bandwidthSize = (ref.endTime - ref.startTime) * stream.bandwidth / 8;
this.downloadManager_.addSegment(
streamSet.type, ref, bandwidthSize, function(id, pos, streamId, data) {
/** @type {shakaExtern.SegmentDataDB} */
var dataDb = {
key: id,
data: data,
manifestKey: this.manifestId_,
streamNumber: streamId,
segmentNumber: pos
};
this.inProgressSegmentIds_.push(id);
return this.dbEngine_.insert('segment', dataDb);
}.bind(this, id, ref.position, stream.id));
segmentsDb.push({
startTime: ref.startTime,
endTime: ref.endTime,
uri: 'offline:' + this.manifestId_ + '/' + stream.id + '/' + id
});
endTime = ref.endTime + period.startTime;
ref = stream.getSegmentReference(++i);
}
this.duration_ = Math.max(this.duration_, (endTime - startTime));
var initUri = null;
if (stream.initSegmentReference) {
var id = this.dbEngine_.reserveId('segment');
initUri = 'offline:' + this.manifestId_ + '/' + stream.id + '/' + id;
this.downloadManager_.addSegment(streamSet.type,
stream.initSegmentReference, 0,
function(streamId, data) {
/** @type {shakaExtern.SegmentDataDB} */
var dataDb = {
key: id,
data: data,
manifestKey: this.manifestId_,
streamNumber: streamId,
segmentNumber: -1
};
this.inProgressSegmentIds_.push(id);
return this.dbEngine_.insert('segment', dataDb);
}.bind(this, stream.id));
}
return {
id: stream.id,
primary: streamSet.primary,
presentationTimeOffset: stream.presentationTimeOffset || 0,
contentType: streamSet.type,
mimeType: stream.mimeType,
codecs: stream.codecs,
frameRate: stream.frameRate,
kind: stream.kind,
language: streamSet.language,
width: stream.width || null,
height: stream.height || null,
initSegmentUri: initUri,
encrypted: stream.encrypted,
keyId: stream.keyId,
segments: segmentsDb
};
};
/**
* Throws an error if the object is destroyed.
* @private
*/
shaka.offline.Storage.prototype.checkDestroyed_ = function() {
if (!this.player_) {
throw new shaka.util.Error(
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.OPERATION_ABORTED);
}
};
shaka.Player.registerSupportPlugin('offline', shaka.offline.Storage.support);
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| all.js | 12.5% | (1 / 8) | 100% | (0 / 0) | 0% | (0 / 2) | 12.5% | (1 / 8) | |
| fullscreen.js | 3.85% | (1 / 26) | 0% | (0 / 18) | 0% | (0 / 4) | 3.85% | (1 / 26) | |
| indexed_db.js | 8.33% | (1 / 12) | 0% | (0 / 4) | 0% | (0 / 1) | 8.33% | (1 / 12) | |
| mediakeys.js | 4.17% | (1 / 24) | 0% | (0 / 12) | 0% | (0 / 1) | 4.17% | (1 / 24) | |
| mediasource.js | 1.33% | (1 / 75) | 0% | (0 / 23) | 0% | (0 / 10) | 1.33% | (1 / 75) | |
| patchedmediakeys_ms.js | 0.75% | (2 / 266) | 0% | (0 / 63) | 0% | (0 / 33) | 0.75% | (2 / 266) | |
| patchedmediakeys_nop.js | 3.23% | (1 / 31) | 0% | (0 / 2) | 0% | (0 / 9) | 3.23% | (1 / 31) | |
| patchedmediakeys_webkit.js | 0.28% | (1 / 357) | 0% | (0 / 112) | 0% | (0 / 39) | 0.28% | (1 / 356) | |
| promise.js | 0.59% | (1 / 169) | 0% | (0 / 55) | 0% | (0 / 24) | 0.6% | (1 / 167) | |
| videoplaybackquality.js | 7.69% | (1 / 13) | 0% | (0 / 6) | 0% | (0 / 2) | 7.69% | (1 / 13) | |
| vttcue.js | 3.33% | (1 / 30) | 0% | (0 / 10) | 0% | (0 / 4) | 3.33% | (1 / 30) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.polyfill.installAll');
goog.provide('shaka.polyfill.register');
/**
* @namespace shaka.polyfill
* @summary A one-stop installer for all polyfills.
* @see http://enwp.org/polyfill
* @exportDoc
*/
/**
* Install all polyfills.
* @export
*/
shaka.polyfill.installAll = function() {
for (var i = 0; i < shaka.polyfill.polyfills_.length; ++i) {
shaka.polyfill.polyfills_[i]();
}
};
/**
* Contains the polyfills that will be installed.
* @private {!Array.<function()>}
*/
shaka.polyfill.polyfills_ = [];
/**
* Registers a new polyfill to be installed.
*
* @param {function()} polyfill
* @export
*/
shaka.polyfill.register = function(polyfill) {
shaka.polyfill.polyfills_.push(polyfill);
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.polyfill.Fullscreen');
goog.require('shaka.polyfill.register');
/**
* @namespace shaka.polyfill.Fullscreen
*
* @summary A polyfill to unify fullscreen APIs across browsers.
* Many browsers have prefixed fullscreen methods on Element and document.
* See {@link http://goo.gl/n7TYl0 Using fullscreen mode} on MDN for more
* information.
*/
/**
* Install the polyfill if needed.
*/
shaka.polyfill.Fullscreen.install = function() {
if (!window.Document) {
// Avoid errors on very old browsers.
return;
}
var proto = Element.prototype;
proto.requestFullscreen = proto.requestFullscreen ||
proto.mozRequestFullScreen ||
proto.msRequestFullscreen ||
proto.webkitRequestFullscreen;
proto = Document.prototype;
proto.exitFullscreen = proto.exitFullscreen ||
proto.mozCancelFullScreen ||
proto.msExitFullscreen ||
proto.webkitExitFullscreen;
if (!('fullscreenElement' in document)) {
Object.defineProperty(document, 'fullscreenElement', {
get: function() {
return document.mozFullScreenElement ||
document.msFullscreenElement ||
document.webkitFullscreenElement;
}
});
Object.defineProperty(document, 'fullscreenEnabled', {
get: function() {
return document.mozFullScreenEnabled ||
document.msFullscreenEnabled ||
document.webkitFullscreenEnabled;
}
});
}
var proxy = shaka.polyfill.Fullscreen.proxyEvent_;
document.addEventListener('webkitfullscreenchange', proxy);
document.addEventListener('webkitfullscreenerror', proxy);
document.addEventListener('mozfullscreenchange', proxy);
document.addEventListener('mozfullscreenerror', proxy);
document.addEventListener('MSFullscreenChange', proxy);
document.addEventListener('MSFullscreenError', proxy);
};
/**
* Proxy fullscreen events after changing their name.
* @param {!Event} event
* @private
*/
shaka.polyfill.Fullscreen.proxyEvent_ = function(event) {
var type2 = event.type.replace(/^(webkit|moz|MS)/, '').toLowerCase();
var event2 = new Event(type2, /** @type {EventInit} */(event));
event.target.dispatchEvent(event2);
};
shaka.polyfill.register(shaka.polyfill.Fullscreen.install);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.polyfill.IndexedDB');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.polyfill.register');
/**
* @namespace shaka.polyfill.IndexedDB
*
* @summary A polyfill to patch indexed db bugs.
*/
/**
* Install the polyfill if needed.
*/
shaka.polyfill.IndexedDB.install = function() {
shaka.log.debug('IndexedDB.install');
var agent = navigator.userAgent;
if (agent && agent.indexOf('CrKey') >= 0) {
shaka.log.debug('Removing IndexedDB from ChromeCast');
delete window.indexedDB;
goog.asserts.assert(
!window.indexedDB, 'Failed to override window.indexedDB');
}
};
shaka.polyfill.register(shaka.polyfill.IndexedDB.install);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.polyfill.MediaKeys');
goog.require('shaka.log');
goog.require('shaka.polyfill.PatchedMediaKeysMs');
goog.require('shaka.polyfill.PatchedMediaKeysNop');
goog.require('shaka.polyfill.PatchedMediaKeysWebkit');
goog.require('shaka.polyfill.register');
/**
* @namespace shaka.polyfill.MediaKeys
*
* @summary A polyfill to unify EME APIs across browser versions.
*
* The {@link https://w3c.github.io/encrypted-media/ EME spec} is still a
* work-in-progress. As such, we need to provide a consistent API to the Shaka
* Player. Until the spec is completely stable, the API provided by this
* polyfill may lag behind the latest spec developments.
*/
/**
* Install the polyfill if needed.
*/
shaka.polyfill.MediaKeys.install = function() {
shaka.log.debug('MediaKeys.install');
if (!window.HTMLVideoElement) {
// Avoid errors on very old browsers.
return;
}
if (navigator.requestMediaKeySystemAccess &&
MediaKeySystemAccess.prototype.getConfiguration) {
shaka.log.info('Using native EME as-is.');
} else if (HTMLMediaElement.prototype.webkitGenerateKeyRequest) {
shaka.log.info('Using webkit-prefixed EME v0.1b');
shaka.polyfill.PatchedMediaKeysWebkit.install('webkit');
} else if (HTMLMediaElement.prototype['generateKeyRequest']) {
// There is a compiler error that would convert
// "HTMLMediaElement.prototype.generateKeyRequest" into
// "HTMLMediaElement.prototype.a". Until resolved, array access
// seems to work ok.
shaka.log.info('Using nonprefixed EME v0.1b');
shaka.polyfill.PatchedMediaKeysWebkit.install('');
} else if (window.MSMediaKeys) {
shaka.log.info('Using ms-prefixed EME v20140218');
shaka.polyfill.PatchedMediaKeysMs.install();
} else {
shaka.log.info('EME not available.');
shaka.polyfill.PatchedMediaKeysNop.install();
}
};
shaka.polyfill.register(shaka.polyfill.MediaKeys.install);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.polyfill.MediaSource');
goog.require('shaka.log');
goog.require('shaka.polyfill.register');
/**
* @namespace shaka.polyfill.MediaSource
*
* @summary A polyfill to patch MSE bugs.
*/
/**
* Install the polyfill if needed.
*/
shaka.polyfill.MediaSource.install = function() {
shaka.log.debug('MediaSource.install');
if (!window.MediaSource) {
shaka.log.info('No MSE implementation available.');
return;
}
// Detection is complicated by the fact that Safari does not expose
// SourceBuffer on window. So we can't detect missing features by accessing
// SourceBuffer.prototype. That is why we use navigator to detect Safari and
// particular versions of it.
var vendor = navigator.vendor;
var version = navigator.appVersion;
if (!vendor || !version || vendor.indexOf('Apple') < 0) {
shaka.log.info('Using native MSE as-is.');
return;
}
if (version.indexOf('Version/8') >= 0) {
// Safari 8 does not implement appendWindowEnd. If we ignore the
// incomplete MSE implementation, some content (especially multi-period)
// will fail to play correctly. The best we can do is blacklist Safari 8.
shaka.log.info('Blacklisting Safari 8 MSE.');
shaka.polyfill.MediaSource.blacklist_();
} else if (version.indexOf('Version/9') >= 0) {
shaka.log.info('Patching Safari 9 MSE bugs.');
// Safari 9 does not correctly implement abort() on SourceBuffer.
// Calling abort() causes a decoder failure, rather than resetting the
// decode timestamp as called for by the spec.
// Bug filed: http://goo.gl/UZ2rPp
shaka.polyfill.MediaSource.stubAbort_();
} else if (version.indexOf('Version/10') >= 0) {
shaka.log.info('Patching Safari 10 MSE bugs.');
// Safari 10 does not correctly implement abort() on SourceBuffer.
// Calling abort() before appending a segment causes that segment to be
// incomplete in buffer.
// Bug filed: https://goo.gl/rC3CLj
shaka.polyfill.MediaSource.stubAbort_();
// Safari 10 fires spurious 'updateend' events after endOfStream().
// Bug filed: https://goo.gl/qCeTZr
shaka.polyfill.MediaSource.patchEndOfStreamEvents_();
} else {
shaka.log.info('Using native MSE as-is.');
}
};
/**
* Blacklist the current browser by making MediaSourceEngine.isBrowserSupported
* fail later.
*
* @private
*/
shaka.polyfill.MediaSource.blacklist_ = function() {
window['MediaSource'] = null;
};
/**
* Stub out abort(). On some buggy MSE implementations, calling abort() causes
* various problems.
*
* @private
*/
shaka.polyfill.MediaSource.stubAbort_ = function() {
var addSourceBuffer = MediaSource.prototype.addSourceBuffer;
MediaSource.prototype.addSourceBuffer = function() {
var sourceBuffer = addSourceBuffer.apply(this, arguments);
sourceBuffer.abort = function() {}; // Stub out for buggy implementations.
return sourceBuffer;
};
};
/**
* Patch endOfStream() to get rid of 'updateend' events that should not fire.
* These extra events confuse MediaSourceEngine, which relies on correct events
* to manage SourceBuffer state.
*
* @private
*/
shaka.polyfill.MediaSource.patchEndOfStreamEvents_ = function() {
var endOfStream = MediaSource.prototype.endOfStream;
MediaSource.prototype.endOfStream = function() {
// This bug manifests only when endOfStream() results in the truncation
// of the MediaSource's duration. So first we must calculate what the
// new duration will be.
var newDuration = 0;
for (var i = 0; i < this.sourceBuffers.length; ++i) {
var buffer = this.sourceBuffers[i];
var bufferEnd = buffer.buffered.end(buffer.buffered.length - 1);
newDuration = Math.max(newDuration, bufferEnd);
}
// If the duration is going to be reduced, suppress the next 'updateend'
// event on each SourceBuffer.
if (!isNaN(this.duration) &&
newDuration < this.duration) {
this.ignoreUpdateEnd_ = true;
for (var i = 0; i < this.sourceBuffers.length; ++i) {
var buffer = this.sourceBuffers[i];
buffer.eventSuppressed_ = false;
}
}
return endOfStream.apply(this, arguments);
};
var addSourceBuffer = MediaSource.prototype.addSourceBuffer;
MediaSource.prototype.addSourceBuffer = function() {
// After adding a new source buffer, add an event listener to allow us to
// suppress events.
var sourceBuffer = addSourceBuffer.apply(this, arguments);
sourceBuffer.mediaSource_ = this;
sourceBuffer.addEventListener('updateend',
shaka.polyfill.MediaSource.ignoreUpdateEnd_, false);
if (!this.cleanUpHandlerInstalled_) {
// If we haven't already, install an event listener to allow us to clean
// up listeners when MediaSource is torn down.
this.addEventListener('sourceclose',
shaka.polyfill.MediaSource.cleanUpListeners_, false);
this.cleanUpHandlerInstalled_ = true;
}
return sourceBuffer;
};
};
/**
* An event listener for 'updateend' which selectively suppresses the events.
*
* @see shaka.polyfill.MediaSource.patchEndOfStreamEvents_
*
* @param {Event} event
* @private
*/
shaka.polyfill.MediaSource.ignoreUpdateEnd_ = function(event) {
var sourceBuffer = event.target;
var mediaSource = sourceBuffer.mediaSource_;
if (mediaSource.ignoreUpdateEnd_) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
sourceBuffer.eventSuppressed_ = true;
for (var i = 0; i < mediaSource.sourceBuffers.length; ++i) {
var buffer = mediaSource.sourceBuffers[i];
if (buffer.eventSuppressed_ == false) {
// More events need to be suppressed.
return;
}
}
// All events have been suppressed, all buffers are out of 'updating'
// mode. Stop suppressing events.
mediaSource.ignoreUpdateEnd_ = false;
}
};
/**
* An event listener for 'sourceclose' which cleans up listeners for 'updateend'
* to avoid memory leaks.
*
* @see shaka.polyfill.MediaSource.patchEndOfStreamEvents_
* @see shaka.polyfill.MediaSource.ignoreUpdateEnd_
*
* @param {Event} event
* @private
*/
shaka.polyfill.MediaSource.cleanUpListeners_ = function(event) {
var mediaSource = event.target;
for (var i = 0; i < mediaSource.sourceBuffers.length; ++i) {
var buffer = mediaSource.sourceBuffers[i];
buffer.removeEventListener('updateend',
shaka.polyfill.MediaSource.ignoreUpdateEnd_, false);
}
mediaSource.removeEventListener('sourceclose',
shaka.polyfill.MediaSource.cleanUpListeners_, false);
};
shaka.polyfill.register(shaka.polyfill.MediaSource.install);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 | 2 1 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.polyfill.PatchedMediaKeysMs');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.util.ArrayUtils');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.Pssh');
goog.require('shaka.util.PublicPromise');
goog.require('shaka.util.Uint8ArrayUtils');
/**
* Install a polyfill to implement {@link http://goo.gl/blgtZZ EME draft
* 12 March 2015} on top of ms-prefixed
* {@link http://www.w3.org/TR/2014/WD-encrypted-media-20140218/ EME v20140218}.
*/
shaka.polyfill.PatchedMediaKeysMs.install = function() {
shaka.log.debug('PatchedMediaKeysMs.install');
// Alias
var PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
// Construct fake key ID. This is not done at load-time to avoid exceptions
// on unsupported browsers. This particular fake key ID was suggested in
// w3c/encrypted-media#32.
PatchedMediaKeysMs.MediaKeyStatusMap.KEY_ID_ = (new Uint8Array([0])).buffer;
// Delete mediaKeys to work around strict mode compatibility issues.
delete HTMLMediaElement.prototype['mediaKeys'];
// Work around read-only declaration for mediaKeys by using a string.
HTMLMediaElement.prototype['mediaKeys'] = null;
HTMLMediaElement.prototype.setMediaKeys = PatchedMediaKeysMs.setMediaKeys;
// Install patches
window.MediaKeys = PatchedMediaKeysMs.MediaKeys;
window.MediaKeySystemAccess = PatchedMediaKeysMs.MediaKeySystemAccess;
navigator.requestMediaKeySystemAccess =
PatchedMediaKeysMs.requestMediaKeySystemAccess;
};
/**
* An implementation of navigator.requestMediaKeySystemAccess.
* Retrieve a MediaKeySystemAccess object.
*
* @this {!Navigator}
* @param {string} keySystem
* @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
* @return {!Promise.<!MediaKeySystemAccess>}
*/
shaka.polyfill.PatchedMediaKeysMs.requestMediaKeySystemAccess =
function(keySystem, supportedConfigurations) {
shaka.log.debug('PatchedMediaKeysMs.requestMediaKeySystemAccess');
goog.asserts.assert(this == navigator,
'bad "this" for requestMediaKeySystemAccess');
// Alias.
var PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
try {
var access = new PatchedMediaKeysMs.MediaKeySystemAccess(
keySystem, supportedConfigurations);
return Promise.resolve(/** @type {!MediaKeySystemAccess} */ (access));
} catch (exception) {
return Promise.reject(exception);
}
};
/**
* An implementation of MediaKeySystemAccess.
*
* @constructor
* @struct
* @param {string} keySystem
* @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
* @implements {MediaKeySystemAccess}
* @throws {Error} if the key system is not supported.
*/
shaka.polyfill.PatchedMediaKeysMs.MediaKeySystemAccess =
function(keySystem, supportedConfigurations) {
shaka.log.debug('PatchedMediaKeysMs.MediaKeySystemAccess');
/** @type {string} */
this.keySystem = keySystem;
/** @private {!MediaKeySystemConfiguration} */
this.configuration_;
var allowPersistentState = true;
var success = false;
for (var i = 0; i < supportedConfigurations.length; ++i) {
var cfg = supportedConfigurations[i];
// Create a new config object and start adding in the pieces which we
// find support for. We will return this from getConfiguration() if
// asked.
/** @type {!MediaKeySystemConfiguration} */
var newCfg = {
'audioCapabilities': [],
'videoCapabilities': [],
// It is technically against spec to return these as optional, but we
// don't truly know their values from the prefixed API:
'persistentState': 'optional',
'distinctiveIdentifier': 'optional',
// Pretend the requested init data types are supported, since we don't
// really know that either:
'initDataTypes': cfg.initDataTypes,
'sessionTypes': ['temporary'],
'label': cfg.label
};
// PatchedMediaKeysMs tests for key system availability through
// MSMediaKeys.isTypeSupported
var ranAnyTests = false;
if (cfg.audioCapabilities) {
for (var j = 0; j < cfg.audioCapabilities.length; ++j) {
var cap = cfg.audioCapabilities[j];
if (cap.contentType) {
ranAnyTests = true;
var contentType = cap.contentType.split(';')[0];
if (MSMediaKeys.isTypeSupported(this.keySystem, contentType)) {
newCfg.audioCapabilities.push(cap);
success = true;
}
}
}
}
if (cfg.videoCapabilities) {
for (var j = 0; j < cfg.videoCapabilities.length; ++j) {
var cap = cfg.videoCapabilities[j];
if (cap.contentType) {
ranAnyTests = true;
var contentType = cap.contentType.split(';')[0];
if (MSMediaKeys.isTypeSupported(this.keySystem, contentType)) {
newCfg.videoCapabilities.push(cap);
success = true;
}
}
}
}
if (!ranAnyTests) {
// If no specific types were requested, we check all common types to find
// out if the key system is present at all.
success = MSMediaKeys.isTypeSupported(this.keySystem, 'video/mp4');
}
if (cfg.persistentState == 'required') {
if (allowPersistentState) {
newCfg.persistentState = 'required';
newCfg.sessionTypes = ['persistent-license'];
} else {
success = false;
}
}
if (success) {
this.configuration_ = newCfg;
return;
}
} // for each cfg in supportedConfigurations
// As per the spec, this should be a DOMException, but
// there is not a public constructor for this
var unsupportedKeySystemError = new Error('Unsupported keySystem');
unsupportedKeySystemError.name = 'NotSupportedError';
unsupportedKeySystemError.code = DOMException.NOT_SUPPORTED_ERR;
throw unsupportedKeySystemError;
};
/** @override */
shaka.polyfill.PatchedMediaKeysMs.MediaKeySystemAccess.prototype.
createMediaKeys = function() {
shaka.log.debug('PatchedMediaKeysMs.MediaKeySystemAccess.createMediaKeys');
// Alias
var PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
var mediaKeys = new PatchedMediaKeysMs.MediaKeys(this.keySystem);
return Promise.resolve(/** @type {!MediaKeys} */ (mediaKeys));
};
/** @override */
shaka.polyfill.PatchedMediaKeysMs.MediaKeySystemAccess.prototype.
getConfiguration = function() {
shaka.log.debug('PatchedMediaKeysMs.MediaKeySystemAccess.getConfiguration');
return this.configuration_;
};
/**
* An implementation of HTMLMediaElement.prototype.setMediaKeys.
* Attach a MediaKeys object to the media element.
*
* @this {!HTMLMediaElement}
* @param {MediaKeys} mediaKeys
* @return {!Promise}
*/
shaka.polyfill.PatchedMediaKeysMs.setMediaKeys = function(mediaKeys) {
shaka.log.debug('PatchedMediaKeysMs.setMediaKeys');
goog.asserts.assert(this instanceof HTMLMediaElement,
'bad "this" for setMediaKeys');
// Alias
var PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
var newMediaKeys =
/** @type {shaka.polyfill.PatchedMediaKeysMs.MediaKeys} */ (
mediaKeys);
var oldMediaKeys =
/** @type {shaka.polyfill.PatchedMediaKeysMs.MediaKeys} */ (
this.mediaKeys);
if (oldMediaKeys && oldMediaKeys != newMediaKeys) {
goog.asserts.assert(oldMediaKeys instanceof PatchedMediaKeysMs.MediaKeys,
'non-polyfill instance of oldMediaKeys');
// Have the old MediaKeys stop listening to events on the video tag.
oldMediaKeys.setMedia(null);
}
delete this['mediaKeys']; // in case there is an existing getter
this['mediaKeys'] = mediaKeys; // work around read-only declaration
if (newMediaKeys) {
goog.asserts.assert(newMediaKeys instanceof PatchedMediaKeysMs.MediaKeys,
'non-polyfill instance of newMediaKeys');
return newMediaKeys.setMedia(this);
}
return Promise.resolve();
};
/**
* An implementation of MediaKeys.
*
* @constructor
* @struct
* @param {string} keySystem
* @implements {MediaKeys}
*/
shaka.polyfill.PatchedMediaKeysMs.MediaKeys = function(keySystem) {
shaka.log.debug('PatchedMediaKeysMs.MediaKeys');
/** @private {!MSMediaKeys} */
this.nativeMediaKeys_ = new MSMediaKeys(keySystem);
/** @private {!shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
};
/** @override */
shaka.polyfill.PatchedMediaKeysMs.MediaKeys.prototype.
createSession = function(opt_sessionType) {
shaka.log.debug('PatchedMediaKeysMs.MediaKeys.createSession');
var sessionType = opt_sessionType || 'temporary';
// For now, only 'temporary' type is supported
if (sessionType != 'temporary') {
throw new TypeError('Session type ' + opt_sessionType +
' is unsupported on this platform.');
}
// Alias
var PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
return new PatchedMediaKeysMs.MediaKeySession(
this.nativeMediaKeys_, sessionType);
};
/** @override */
shaka.polyfill.PatchedMediaKeysMs.MediaKeys.prototype.
setServerCertificate = function(serverCertificate) {
shaka.log.debug('PatchedMediaKeysMs.MediaKeys.setServerCertificate');
// There is no equivalent in PatchedMediaKeysMs, so return failure.
return Promise.reject(new Error('setServerCertificate not supported on ' +
'this platform.'));
};
/**
* @param {HTMLMediaElement} media
* @protected
* @return {!Promise}
*/
shaka.polyfill.PatchedMediaKeysMs.MediaKeys.prototype.
setMedia = function(media) {
// Alias
var PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
// Remove any old listeners.
this.eventManager_.removeAll();
// It is valid for media to be null, it's used to flag that event handlers
// need to be cleaned up
if (!media) {
return Promise.resolve();
}
// Intercept and translate these prefixed EME events.
this.eventManager_.listen(media, 'msneedkey',
/** @type {shaka.util.EventManager.ListenerType} */
(PatchedMediaKeysMs.onMsNeedKey_));
var self = this;
function setMediaKeysDeferred() {
media.msSetMediaKeys(self.nativeMediaKeys_);
media.removeEventListener('loadedmetadata', setMediaKeysDeferred);
}
// Wrap native HTMLMediaElement.msSetMediaKeys with Promise
try {
// IE11/Edge requires that readyState >=1 before mediaKeys can be set, so
// check this and wait for loadedmetadata if we are not in the correct state
if (media.readyState >= 1) {
media.msSetMediaKeys(this.nativeMediaKeys_);
} else {
media.addEventListener('loadedmetadata', setMediaKeysDeferred);
}
return Promise.resolve();
} catch (exception) {
return Promise.reject(exception);
}
};
/**
* An implementation of MediaKeySession.
*
* @constructor
* @struct
* @param {MSMediaKeys} nativeMediaKeys
* @param {string} sessionType
* @implements {MediaKeySession}
* @extends {shaka.util.FakeEventTarget}
*/
shaka.polyfill.PatchedMediaKeysMs.
MediaKeySession = function(nativeMediaKeys, sessionType) {
shaka.log.debug('PatchedMediaKeysMs.MediaKeySession');
shaka.util.FakeEventTarget.call(this);
// Native MediaKeySession, which will be created in generateRequest
/** @private {MSMediaKeySession} */
this.nativeMediaKeySession_ = null;
/** @private {MSMediaKeys} */
this.nativeMediaKeys_ = nativeMediaKeys;
// Promises that are resolved later
/** @private {Promise} */
this.generateRequestPromise_ = null;
/** @private {Promise} */
this.updatePromise_ = null;
/** @private {!shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/** @type {string} */
this.sessionId = '';
/** @type {number} */
this.expiration = NaN;
/** @type {!shaka.util.PublicPromise} */
this.closed = new shaka.util.PublicPromise();
/** @type {!MediaKeyStatusMap} */
this.keyStatuses =
new shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap();
};
goog.inherits(shaka.polyfill.PatchedMediaKeysMs.MediaKeySession,
shaka.util.FakeEventTarget);
/** @override */
shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
generateRequest = function(initDataType, initData) {
shaka.log.debug('PatchedMediaKeysMs.MediaKeySession.generateRequest');
this.generateRequestPromise_ = new shaka.util.PublicPromise();
try {
// This EME spec version requires a MIME content type as the 1st param
// to createSession, but doesn't seem to matter what the value is.
// NOTE: IE11 takes either Uint8Array or ArrayBuffer, but Edge 12 only
// accepts Uint8Array.
this.nativeMediaKeySession_ = this.nativeMediaKeys_
.createSession('video/mp4', new Uint8Array(initData), null);
// Attach session event handlers here
this.eventManager_.listen(this.nativeMediaKeySession_, 'mskeymessage',
/** @type {shaka.util.EventManager.ListenerType} */
(this.onMsKeyMessage_.bind(this)));
this.eventManager_.listen(this.nativeMediaKeySession_, 'mskeyadded',
/** @type {shaka.util.EventManager.ListenerType} */
(this.onMsKeyAdded_.bind(this)));
this.eventManager_.listen(this.nativeMediaKeySession_, 'mskeyerror',
/** @type {shaka.util.EventManager.ListenerType} */
(this.onMsKeyError_.bind(this)));
this.updateKeyStatus_('status-pending');
} catch (exception) {
this.generateRequestPromise_.reject(exception);
}
return this.generateRequestPromise_;
};
/** @override */
shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
load = function() {
shaka.log.debug('PatchedMediaKeysMs.MediaKeySession.load');
return Promise.reject(new Error('MediaKeySession.load not yet supported'));
};
/** @override */
shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
update = function(response) {
shaka.log.debug('PatchedMediaKeysMs.MediaKeySession.update');
this.updatePromise_ = new shaka.util.PublicPromise();
try {
// Pass through to the native session.
// NOTE: IE11 takes either Uint8Array or ArrayBuffer, but Edge 12 only
// accepts Uint8Array.
this.nativeMediaKeySession_.update(new Uint8Array(response));
} catch (exception) {
this.updatePromise_.reject(exception);
}
return this.updatePromise_;
};
/** @override */
shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
close = function() {
shaka.log.debug('PatchedMediaKeysMs.MediaKeySession.close');
try {
// Pass through to the native session
// NOTE: IE seems to have spec discrepancy here - v2010218 should have
// MediaKeySession.release, but actually uses "close". The next version
// of the spec is the initial Promise based one, so it's not the target spec
// either.
this.nativeMediaKeySession_.close();
this.closed.resolve();
this.eventManager_.removeAll();
} catch (exception) {
this.closed.reject(exception);
}
return this.closed;
};
/** @override */
shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
remove = function() {
shaka.log.debug('PatchedMediaKeysMs.MediaKeySession.remove');
return Promise.reject(new Error('MediaKeySession.remove is only ' +
'applicable for persistent licenses, which are not supported on ' +
'this platform'));
};
/**
* Handler for the native media elements msNeedKey event.
*
* @this {!HTMLMediaElement}
* @param {!MediaKeyEvent} event
* @private
*/
shaka.polyfill.PatchedMediaKeysMs.onMsNeedKey_ = function(event) {
shaka.log.debug('PatchedMediaKeysMs.onMsNeedKey_', event);
// Alias
var PatchedMediaKeysMs = shaka.polyfill.PatchedMediaKeysMs;
// NOTE: Because "this" is a real EventTarget, on IE, the event we dispatch
// here must also be a real Event.
var event2 = /** @type {!CustomEvent} */(document.createEvent('CustomEvent'));
event2.initCustomEvent('encrypted', false, false, null);
event2.initDataType = 'cenc';
event2.initData = PatchedMediaKeysMs.NormaliseInitData_(event.initData);
this.dispatchEvent(event2);
};
/**
* Normalise the initData array. This is to apply browser specific work-arounds,
* e.g. removing duplicates which appears to occur intermittently when the
* native msneedkey event fires (i.e. event.initData contains dupes).
*
* @param {?Uint8Array} initData
* @private
* @return {?Uint8Array}
*/
shaka.polyfill.PatchedMediaKeysMs.
NormaliseInitData_ = function(initData) {
if (!initData) {
return initData;
}
var pssh = new shaka.util.Pssh(initData);
// If there is only a single pssh, return the original array
if (pssh.dataBoundaries.length <= 1) {
return initData;
}
var unfilteredInitDatas = [];
for (var i = 0; i < pssh.dataBoundaries.length; i++) {
var currPssh = initData.subarray(
pssh.dataBoundaries[i].start,
pssh.dataBoundaries[i].end + 1); // end is exclusive, hence the +1
unfilteredInitDatas.push(currPssh);
}
// Dedupe psshData
var dedupedInitDatas = shaka.util.ArrayUtils.removeDuplicates(
unfilteredInitDatas,
shaka.polyfill.PatchedMediaKeysMs.compareInitDatas_);
var targetLength = 0;
for (var i = 0; i < dedupedInitDatas.length; i++) {
targetLength += dedupedInitDatas[i].length;
}
// Concat array of Uint8Arrays back into a single Uint8Array
var normalisedInitData = new Uint8Array(targetLength);
var offset = 0;
for (var i = 0; i < dedupedInitDatas.length; i++) {
normalisedInitData.set(dedupedInitDatas[i], offset);
offset += dedupedInitDatas[i].length;
}
return normalisedInitData;
};
/**
* @param {!Uint8Array} initDataA
* @param {!Uint8Array} initDataB
* @return {boolean}
* @private
*/
shaka.polyfill.PatchedMediaKeysMs.compareInitDatas_ =
function(initDataA, initDataB) {
return shaka.util.Uint8ArrayUtils.equal(initDataA, initDataB);
};
/**
* Handler for the native keymessage event on MSMediaKeySession.
*
* @param {!MediaKeyEvent} event
* @private
*/
shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
onMsKeyMessage_ = function(event) {
shaka.log.debug('PatchedMediaKeysMs.onMsKeyMessage_', event);
// We can now resolve this.generateRequestPromise (it should be non-null)
goog.asserts.assert(this.generateRequestPromise_,
'generateRequestPromise_ not set in onMsKeyMessage_');
if (this.generateRequestPromise_) {
this.generateRequestPromise_.resolve();
this.generateRequestPromise_ = null;
}
var isNew = this.keyStatuses.getStatus() == undefined;
var event2 = new shaka.util.FakeEvent('message', {
messageType: isNew ? 'licenserequest' : 'licenserenewal',
message: event.message.buffer
});
this.dispatchEvent(event2);
};
/**
* Handler for the native keyadded event on MSMediaKeySession.
*
* @param {!MediaKeyEvent} event
* @private
*/
shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
onMsKeyAdded_ = function(event) {
shaka.log.debug('PatchedMediaKeysMs.onMsKeyAdded_', event);
// PlayReady's concept of persistent licenses makes emulation difficult here.
// A license policy can say that the license persists, which causes the CDM to
// store it for use in a later session. The result is that in IE11, the CDM
// fires 'mskeyadded' without ever firing 'mskeymessage'.
if (this.generateRequestPromise_) {
shaka.log.debug('Simulating completion for a PR persistent license.');
goog.asserts.assert(!this.updatePromise_,
'updatePromise_ and generateRequestPromise_ set in onMsKeyAdded_');
this.updateKeyStatus_('usable');
this.generateRequestPromise_.resolve();
this.generateRequestPromise_ = null;
return;
}
// We can now resolve this.updatePromise (it should be non-null)
goog.asserts.assert(this.updatePromise_,
'updatePromise_ not set in onMsKeyAdded_');
if (this.updatePromise_) {
this.updateKeyStatus_('usable');
this.updatePromise_.resolve();
this.updatePromise_ = null;
}
};
/**
* Handler for the native keyerror event on MSMediaKeySession.
*
* @param {!MediaKeyEvent} event
* @private
*/
shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
onMsKeyError_ = function(event) {
shaka.log.debug('PatchedMediaKeysMs.onMsKeyError_', event);
var error = new Error('EME PatchedMediaKeysMs key error');
error.errorCode = this.nativeMediaKeySession_.error;
if (this.generateRequestPromise_ != null) {
this.generateRequestPromise_.reject(error);
this.generateRequestPromise_ = null;
} else if (this.updatePromise_ != null) {
this.updatePromise_.reject(error);
this.updatePromise_ = null;
} else {
/*
Unexpected error - map native codes to standardised key statuses.
Possible values of this.nativeMediaKeySession_.error.code
MS_MEDIA_KEYERR_UNKNOWN = 1
MS_MEDIA_KEYERR_CLIENT = 2
MS_MEDIA_KEYERR_SERVICE = 3
MS_MEDIA_KEYERR_OUTPUT = 4
MS_MEDIA_KEYERR_HARDWARECHANGE = 5
MS_MEDIA_KEYERR_DOMAIN = 6
*/
switch (this.nativeMediaKeySession_.error.code) {
case MSMediaKeyError.MS_MEDIA_KEYERR_OUTPUT:
case MSMediaKeyError.MS_MEDIA_KEYERR_HARDWARECHANGE:
this.updateKeyStatus_('output-not-allowed');
default:
this.updateKeyStatus_('internal-error');
}
}
};
/**
* Update key status and dispatch a 'keystatuseschange' event.
*
* @param {string} status
* @private
*/
shaka.polyfill.PatchedMediaKeysMs.MediaKeySession.prototype.
updateKeyStatus_ = function(status) {
this.keyStatuses.setStatus(status);
var event = new shaka.util.FakeEvent('keystatuseschange');
this.dispatchEvent(event);
};
/**
* An implementation of MediaKeyStatusMap.
* This fakes a map with a single key ID.
*
* @constructor
* @struct
* @implements {MediaKeyStatusMap}
*/
shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap = function() {
/**
* @type {number}
*/
this.size = 0;
/**
* @private {string|undefined}
*/
this.status_ = undefined;
};
/**
* @const {!ArrayBuffer}
* @private
*/
shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.KEY_ID_;
/**
* An internal method used by the session to set key status.
* @param {string|undefined} status
*/
shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
setStatus = function(status) {
this.size = status == undefined ? 0 : 1;
this.status_ = status;
};
/**
* An internal method used by the session to get key status.
* @return {string|undefined}
*/
shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
getStatus = function() {
return this.status_;
};
/** @override */
shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
forEach = function(fn) {
if (this.status_) {
var fakeKeyId =
shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.KEY_ID_;
fn(this.status_, fakeKeyId);
}
};
/** @override */
shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
get = function(keyId) {
if (this.has(keyId)) {
return this.status_;
}
return undefined;
};
/** @override */
shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
has = function(keyId) {
var fakeKeyId =
shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.KEY_ID_;
if (this.status_ &&
shaka.util.Uint8ArrayUtils.equal(
new Uint8Array(keyId), new Uint8Array(fakeKeyId))) {
return true;
}
return false;
};
/** @suppress {missingReturn} */
shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
entries = function() {
goog.asserts.assert(false, 'Not used! Provided only for compiler.');
};
/** @suppress {missingReturn} */
shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
keys = function() {
goog.asserts.assert(false, 'Not used! Provided only for compiler.');
};
/** @suppress {missingReturn} */
shaka.polyfill.PatchedMediaKeysMs.MediaKeyStatusMap.prototype.
values = function() {
goog.asserts.assert(false, 'Not used! Provided only for compiler.');
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.polyfill.PatchedMediaKeysNop');
goog.require('goog.asserts');
goog.require('shaka.log');
/**
* Install a polyfill to stub out {@link http://goo.gl/blgtZZ EME draft
* 12 March 2015} on browsers without EME. All methods will fail.
*/
shaka.polyfill.PatchedMediaKeysNop.install = function() {
shaka.log.debug('PatchedMediaKeysNop.install');
// Alias.
var PatchedMediaKeysNop = shaka.polyfill.PatchedMediaKeysNop;
// Install patches.
navigator.requestMediaKeySystemAccess =
PatchedMediaKeysNop.requestMediaKeySystemAccess;
// Delete mediaKeys to work around strict mode compatibility issues.
delete HTMLMediaElement.prototype['mediaKeys'];
// Work around read-only declaration for mediaKeys by using a string.
HTMLMediaElement.prototype['mediaKeys'] = null;
HTMLMediaElement.prototype.setMediaKeys = PatchedMediaKeysNop.setMediaKeys;
// These are not usable, but allow Player.isBrowserSupported to pass.
window.MediaKeys = PatchedMediaKeysNop.MediaKeys;
window.MediaKeySystemAccess = PatchedMediaKeysNop.MediaKeySystemAccess;
};
/**
* An implementation of navigator.requestMediaKeySystemAccess.
* Retrieve a MediaKeySystemAccess object.
*
* @this {!Navigator}
* @param {string} keySystem
* @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
* @return {!Promise.<!MediaKeySystemAccess>}
*/
shaka.polyfill.PatchedMediaKeysNop.requestMediaKeySystemAccess =
function(keySystem, supportedConfigurations) {
shaka.log.debug('PatchedMediaKeysNop.requestMediaKeySystemAccess');
goog.asserts.assert(this == navigator,
'bad "this" for requestMediaKeySystemAccess');
return Promise.reject(new Error(
'The key system specified is not supported.'));
};
/**
* An implementation of HTMLMediaElement.prototype.setMediaKeys.
* Attach a MediaKeys object to the media element.
*
* @this {!HTMLMediaElement}
* @param {MediaKeys} mediaKeys
* @return {!Promise}
*/
shaka.polyfill.PatchedMediaKeysNop.setMediaKeys = function(mediaKeys) {
shaka.log.debug('PatchedMediaKeysNop.setMediaKeys');
goog.asserts.assert(this instanceof HTMLMediaElement,
'bad "this" for setMediaKeys');
if (mediaKeys == null) {
return Promise.resolve();
}
return Promise.reject(new Error('MediaKeys not supported.'));
};
/**
* An unusable constructor for MediaKeys.
* @constructor
* @struct
* @implements {MediaKeys}
*/
shaka.polyfill.PatchedMediaKeysNop.MediaKeys = function() {
throw new TypeError('Illegal constructor.');
};
/** @override */
shaka.polyfill.PatchedMediaKeysNop.MediaKeys.prototype.createSession =
function() {};
/** @override */
shaka.polyfill.PatchedMediaKeysNop.MediaKeys.prototype.setServerCertificate =
function() {};
/**
* An unusable constructor for MediaKeySystemAccess.
* @constructor
* @struct
* @implements {MediaKeySystemAccess}
*/
shaka.polyfill.PatchedMediaKeysNop.MediaKeySystemAccess = function() {
throw new TypeError('Illegal constructor.');
};
/** @override */
shaka.polyfill.PatchedMediaKeysNop.MediaKeySystemAccess.prototype.
getConfiguration = function() {};
/** @override */
shaka.polyfill.PatchedMediaKeysNop.MediaKeySystemAccess.prototype.
createMediaKeys = function() {};
/** @override */
shaka.polyfill.PatchedMediaKeysNop.MediaKeySystemAccess.prototype.
keySystem;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.polyfill.PatchedMediaKeysWebkit');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.PublicPromise');
goog.require('shaka.util.StringUtils');
goog.require('shaka.util.Uint8ArrayUtils');
/**
* Store api prefix.
*
* @private {string}
*/
shaka.polyfill.PatchedMediaKeysWebkit.prefix_ = '';
/**
* Install a polyfill to implement {@link http://goo.gl/blgtZZ EME draft
* 12 March 2015} on top of webkit-prefixed
* {@link http://goo.gl/FSpoAo EME v0.1b}.
*
* @param {string} prefix
*/
shaka.polyfill.PatchedMediaKeysWebkit.install = function(prefix) {
shaka.log.debug('PatchedMediaKeysWebkit.install');
// Alias.
var PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
PatchedMediaKeysWebkit.prefix_ = prefix;
var prefixApi = PatchedMediaKeysWebkit.prefixApi_;
goog.asserts.assert(
HTMLMediaElement.prototype[prefixApi('generateKeyRequest')],
'PatchedMediaKeysWebkit APIs not available!');
// Construct fake key ID. This is not done at load-time to avoid exceptions
// on unsupported browsers. This particular fake key ID was suggested in
// w3c/encrypted-media#32.
PatchedMediaKeysWebkit.MediaKeyStatusMap.KEY_ID_ =
(new Uint8Array([0])).buffer;
// Install patches.
navigator.requestMediaKeySystemAccess =
PatchedMediaKeysWebkit.requestMediaKeySystemAccess;
// Delete mediaKeys to work around strict mode compatibility issues.
delete HTMLMediaElement.prototype['mediaKeys'];
// Work around read-only declaration for mediaKeys by using a string.
HTMLMediaElement.prototype['mediaKeys'] = null;
HTMLMediaElement.prototype.setMediaKeys = PatchedMediaKeysWebkit.setMediaKeys;
window.MediaKeys = PatchedMediaKeysWebkit.MediaKeys;
window.MediaKeySystemAccess = PatchedMediaKeysWebkit.MediaKeySystemAccess;
};
/**
* Prefix api by stored prefix.
*
* @param {string} api
* @return {string}
* @private
*/
shaka.polyfill.PatchedMediaKeysWebkit.prefixApi_ = function(api) {
var prefix = shaka.polyfill.PatchedMediaKeysWebkit.prefix_;
if (prefix) {
return prefix + api.charAt(0).toUpperCase() + api.slice(1);
}
return api;
};
/**
* An implementation of navigator.requestMediaKeySystemAccess.
* Retrieve a MediaKeySystemAccess object.
*
* @this {!Navigator}
* @param {string} keySystem
* @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
* @return {!Promise.<!MediaKeySystemAccess>}
*/
shaka.polyfill.PatchedMediaKeysWebkit.requestMediaKeySystemAccess =
function(keySystem, supportedConfigurations) {
shaka.log.debug('PatchedMediaKeysWebkit.requestMediaKeySystemAccess');
goog.asserts.assert(this == navigator,
'bad "this" for requestMediaKeySystemAccess');
// Alias.
var PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
try {
var access = new PatchedMediaKeysWebkit.MediaKeySystemAccess(
keySystem, supportedConfigurations);
return Promise.resolve(/** @type {!MediaKeySystemAccess} */ (access));
} catch (exception) {
return Promise.reject(exception);
}
};
/**
* An implementation of HTMLMediaElement.prototype.setMediaKeys.
* Attach a MediaKeys object to the media element.
*
* @this {!HTMLMediaElement}
* @param {MediaKeys} mediaKeys
* @return {!Promise}
*/
shaka.polyfill.PatchedMediaKeysWebkit.setMediaKeys = function(mediaKeys) {
shaka.log.debug('PatchedMediaKeysWebkit.setMediaKeys');
goog.asserts.assert(this instanceof HTMLMediaElement,
'bad "this" for setMediaKeys');
// Alias.
var PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
var newMediaKeys =
/** @type {shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys} */ (
mediaKeys);
var oldMediaKeys =
/** @type {shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys} */ (
this.mediaKeys);
if (oldMediaKeys && oldMediaKeys != newMediaKeys) {
goog.asserts.assert(
oldMediaKeys instanceof PatchedMediaKeysWebkit.MediaKeys,
'non-polyfill instance of oldMediaKeys');
// Have the old MediaKeys stop listening to events on the video tag.
oldMediaKeys.setMedia(null);
}
delete this['mediaKeys']; // in case there is an existing getter
this['mediaKeys'] = mediaKeys; // work around read-only declaration
if (newMediaKeys) {
goog.asserts.assert(
newMediaKeys instanceof PatchedMediaKeysWebkit.MediaKeys,
'non-polyfill instance of newMediaKeys');
newMediaKeys.setMedia(this);
}
return Promise.resolve();
};
/**
* For some of this polyfill's implementation, we need to query a video element.
* But for some embedded systems, it is memory-expensive to create multiple
* video elements. Therefore, we check the document to see if we can borrow one
* to query before we fall back to creating one temporarily.
*
* @return {!HTMLVideoElement}
* @private
*/
shaka.polyfill.PatchedMediaKeysWebkit.getVideoElement_ = function() {
var videos = document.getElementsByTagName('video');
var tmpVideo = videos.length ? videos[0] : document.createElement('video');
return /** @type {!HTMLVideoElement} */(tmpVideo);
};
/**
* An implementation of MediaKeySystemAccess.
*
* @constructor
* @struct
* @param {string} keySystem
* @param {!Array.<!MediaKeySystemConfiguration>} supportedConfigurations
* @implements {MediaKeySystemAccess}
* @throws {Error} if the key system is not supported.
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySystemAccess =
function(keySystem, supportedConfigurations) {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySystemAccess');
/** @type {string} */
this.keySystem = keySystem;
/** @private {string} */
this.internalKeySystem_ = keySystem;
/** @private {!MediaKeySystemConfiguration} */
this.configuration_;
// This is only a guess, since we don't really know from the prefixed API.
var allowPersistentState = true;
if (keySystem == 'org.w3.clearkey') {
// ClearKey's string must be prefixed in v0.1b.
this.internalKeySystem_ = 'webkit-org.w3.clearkey';
// ClearKey doesn't support persistence.
allowPersistentState = false;
}
var success = false;
var tmpVideo = shaka.polyfill.PatchedMediaKeysWebkit.getVideoElement_();
for (var i = 0; i < supportedConfigurations.length; ++i) {
var cfg = supportedConfigurations[i];
// Create a new config object and start adding in the pieces which we
// find support for. We will return this from getConfiguration() if
// asked.
/** @type {!MediaKeySystemConfiguration} */
var newCfg = {
'audioCapabilities': [],
'videoCapabilities': [],
// It is technically against spec to return these as optional, but we
// don't truly know their values from the prefixed API:
'persistentState': 'optional',
'distinctiveIdentifier': 'optional',
// Pretend the requested init data types are supported, since we don't
// really know that either:
'initDataTypes': cfg.initDataTypes,
'sessionTypes': ['temporary'],
'label': cfg.label
};
// v0.1b tests for key system availability with an extra argument on
// canPlayType.
var ranAnyTests = false;
if (cfg.audioCapabilities) {
for (var j = 0; j < cfg.audioCapabilities.length; ++j) {
var cap = cfg.audioCapabilities[j];
if (cap.contentType) {
ranAnyTests = true;
// In Chrome <= 40, if you ask about Widevine-encrypted audio support,
// you get a false-negative when you specify codec information.
// Work around this by stripping codec info for audio types.
var contentType = cap.contentType.split(';')[0];
if (tmpVideo.canPlayType(contentType, this.internalKeySystem_)) {
newCfg.audioCapabilities.push(cap);
success = true;
}
}
}
}
if (cfg.videoCapabilities) {
for (var j = 0; j < cfg.videoCapabilities.length; ++j) {
var cap = cfg.videoCapabilities[j];
if (cap.contentType) {
ranAnyTests = true;
if (tmpVideo.canPlayType(cap.contentType, this.internalKeySystem_)) {
newCfg.videoCapabilities.push(cap);
success = true;
}
}
}
}
if (!ranAnyTests) {
// If no specific types were requested, we check all common types to find
// out if the key system is present at all.
success = tmpVideo.canPlayType('video/mp4', this.internalKeySystem_) ||
tmpVideo.canPlayType('video/webm', this.internalKeySystem_);
}
if (cfg.persistentState == 'required') {
if (allowPersistentState) {
newCfg.persistentState = 'required';
newCfg.sessionTypes = ['persistent-license'];
} else {
success = false;
}
}
if (success) {
this.configuration_ = newCfg;
return;
}
} // for each cfg in supportedConfigurations
var message = 'Unsupported keySystem';
if (keySystem == 'org.w3.clearkey' || keySystem == 'com.widevine.alpha') {
message = 'None of the requested configurations were supported.';
}
var unsupportedError = new Error(message);
unsupportedError.name = 'NotSupportedError';
unsupportedError.code = DOMException.NOT_SUPPORTED_ERR;
throw unsupportedError;
};
/** @override */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySystemAccess.prototype.
createMediaKeys = function() {
shaka.log.debug(
'PatchedMediaKeysWebkit.MediaKeySystemAccess.createMediaKeys');
// Alias.
var PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
var mediaKeys = new PatchedMediaKeysWebkit.MediaKeys(this.internalKeySystem_);
return Promise.resolve(/** @type {!MediaKeys} */ (mediaKeys));
};
/** @override */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySystemAccess.prototype.
getConfiguration = function() {
shaka.log.debug(
'PatchedMediaKeysWebkit.MediaKeySystemAccess.getConfiguration');
return this.configuration_;
};
/**
* An implementation of MediaKeys.
*
* @constructor
* @struct
* @param {string} keySystem
* @implements {MediaKeys}
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys = function(keySystem) {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeys');
/** @private {string} */
this.keySystem_ = keySystem;
/** @private {HTMLMediaElement} */
this.media_ = null;
/** @private {!shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/**
* @private {!Array.<!shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession>}
*/
this.newSessions_ = [];
/**
* @private {!Object.<string,
* !shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession>}
*/
this.sessionMap_ = {};
};
/**
* @param {HTMLMediaElement} media
* @protected
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys.prototype.setMedia =
function(media) {
this.media_ = media;
// Remove any old listeners.
this.eventManager_.removeAll();
var prefix = shaka.polyfill.PatchedMediaKeysWebkit.prefix_;
if (media) {
// Intercept and translate these prefixed EME events.
this.eventManager_.listen(media, prefix + 'needkey',
/** @type {shaka.util.EventManager.ListenerType} */ (
this.onWebkitNeedKey_.bind(this)));
this.eventManager_.listen(media, prefix + 'keymessage',
/** @type {shaka.util.EventManager.ListenerType} */ (
this.onWebkitKeyMessage_.bind(this)));
this.eventManager_.listen(media, prefix + 'keyadded',
/** @type {shaka.util.EventManager.ListenerType} */ (
this.onWebkitKeyAdded_.bind(this)));
this.eventManager_.listen(media, prefix + 'keyerror',
/** @type {shaka.util.EventManager.ListenerType} */ (
this.onWebkitKeyError_.bind(this)));
}
};
/** @override */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys.prototype.createSession =
function(opt_sessionType) {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeys.createSession');
var sessionType = opt_sessionType || 'temporary';
// TODO: Consider adding support for persistent-release once Chrome has
// implemented it natively. http://crbug.com/448888
// This is a non-issue if we've deprecated the polyfill by then, since
// prefixed EME is on its way out.
if (sessionType != 'temporary' && sessionType != 'persistent-license') {
throw new TypeError('Session type ' + opt_sessionType +
' is unsupported on this platform.');
}
// Alias.
var PatchedMediaKeysWebkit = shaka.polyfill.PatchedMediaKeysWebkit;
// Unprefixed EME allows for session creation without a video tag or src.
// Prefixed EME requires both a valid HTMLMediaElement and a src.
var media = this.media_ || /** @type {!HTMLMediaElement} */(
document.createElement('video'));
if (!media.src) media.src = 'about:blank';
var session = new PatchedMediaKeysWebkit.MediaKeySession(
media, this.keySystem_, sessionType);
this.newSessions_.push(session);
return session;
};
/** @override */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys.prototype.setServerCertificate =
function(serverCertificate) {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeys.setServerCertificate');
// There is no equivalent in v0.1b, so return failure.
return Promise.reject(new Error(
'setServerCertificate not supported on this platform.'));
};
/**
* @param {!MediaKeyEvent} event
* @private
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys.prototype.onWebkitNeedKey_ =
function(event) {
shaka.log.debug('PatchedMediaKeysWebkit.onWebkitNeedKey_', event);
goog.asserts.assert(this.media_, 'media_ not set in onWebkitNeedKey_');
var event2 = document.createEvent('CustomEvent');
event2.initCustomEvent('encrypted', false, false, null);
// not used by v0.1b EME, but given a valid value
event2.initDataType = 'webm';
event2.initData = event.initData;
this.media_.dispatchEvent(event2);
};
/**
* @param {!MediaKeyEvent} event
* @private
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys.prototype.onWebkitKeyMessage_ =
function(event) {
shaka.log.debug('PatchedMediaKeysWebkit.onWebkitKeyMessage_', event);
var session = this.findSession_(event.sessionId);
if (!session) {
shaka.log.error('Session not found', event.sessionId);
return;
}
var isNew = session.keyStatuses.getStatus() == undefined;
var event2 = new shaka.util.FakeEvent('message', {
messageType: isNew ? 'licenserequest' : 'licenserenewal',
message: event.message
});
session.generated();
session.dispatchEvent(event2);
};
/**
* @param {!MediaKeyEvent} event
* @private
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys.prototype.onWebkitKeyAdded_ =
function(event) {
shaka.log.debug('PatchedMediaKeysWebkit.onWebkitKeyAdded_', event);
var session = this.findSession_(event.sessionId);
goog.asserts.assert(session, 'unable to find session in onWebkitKeyAdded_');
if (session) {
session.ready();
}
};
/**
* @param {!MediaKeyEvent} event
* @private
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys.prototype.onWebkitKeyError_ =
function(event) {
shaka.log.debug('PatchedMediaKeysWebkit.onWebkitKeyError_', event);
var session = this.findSession_(event.sessionId);
goog.asserts.assert(session, 'unable to find session in onWebkitKeyError_');
if (session) {
session.handleError(event);
}
};
/**
* @param {string} sessionId
* @return {shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession}
* @private
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys.prototype.findSession_ =
function(sessionId) {
var session = this.sessionMap_[sessionId];
if (session) {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeys.findSession_', session);
return session;
}
session = this.newSessions_.shift();
if (session) {
session.sessionId = sessionId;
this.sessionMap_[sessionId] = session;
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeys.findSession_', session);
return session;
}
return null;
};
/**
* An implementation of MediaKeySession.
*
* @param {!HTMLMediaElement} media
* @param {string} keySystem
* @param {string} sessionType
*
* @constructor
* @struct
* @implements {MediaKeySession}
* @extends {shaka.util.FakeEventTarget}
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession =
function(media, keySystem, sessionType) {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession');
shaka.util.FakeEventTarget.call(this);
/** @private {!HTMLMediaElement} */
this.media_ = media;
/** @private {boolean} */
this.initialized_ = false;
/** @private {shaka.util.PublicPromise} */
this.generatePromise_ = null;
/** @private {shaka.util.PublicPromise} */
this.updatePromise_ = null;
/** @private {string} */
this.keySystem_ = keySystem;
/** @private {string} */
this.type_ = sessionType;
/** @type {string} */
this.sessionId = '';
/** @type {number} */
this.expiration = NaN;
/** @type {!shaka.util.PublicPromise} */
this.closed = new shaka.util.PublicPromise();
/** @type {!MediaKeyStatusMap} */
this.keyStatuses =
new shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap();
};
goog.inherits(shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession,
shaka.util.FakeEventTarget);
/**
* Signals that the license request has been generated. This resolves the
* 'generateRequest' promise.
*
* @protected
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession.prototype.generated =
function() {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.generated');
if (this.generatePromise_) {
this.generatePromise_.resolve();
this.generatePromise_ = null;
}
};
/**
* Signals that the session is 'ready', which is the terminology used in older
* versions of EME. The new signal is to resolve the 'update' promise. This
* translates between the two.
*
* @protected
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession.prototype.ready =
function() {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.ready');
this.updateKeyStatus_('usable');
if (this.updatePromise_) {
this.updatePromise_.resolve();
}
this.updatePromise_ = null;
};
/**
* Either rejects a promise, or dispatches an error event, as appropriate.
*
* @param {!MediaKeyEvent} event
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession.prototype.handleError =
function(event) {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.handleError', event);
// This does not match the DOMException we get in current WD EME, but it will
// at least provide some information which can be used to look into the
// problem.
var error = new Error('EME v0.1b key error');
error.errorCode = event.errorCode;
error.errorCode.systemCode = event.systemCode;
// The presence or absence of sessionId indicates whether this corresponds to
// generateRequest() or update().
if (!event.sessionId && this.generatePromise_) {
error.method = 'generateRequest';
if (event.systemCode == 45) {
error.message = 'Unsupported session type.';
}
this.generatePromise_.reject(error);
this.generatePromise_ = null;
} else if (event.sessionId && this.updatePromise_) {
error.method = 'update';
this.updatePromise_.reject(error);
this.updatePromise_ = null;
} else {
// This mapping of key statuses is imperfect at best.
var code = event.errorCode.code;
var systemCode = event.systemCode;
if (code == MediaKeyError['MEDIA_KEYERR_OUTPUT']) {
this.updateKeyStatus_('output-restricted');
} else if (systemCode == 1) {
this.updateKeyStatus_('expired');
} else {
this.updateKeyStatus_('internal-error');
}
}
};
/**
* Logic which is shared between generateRequest() and load(), both of which
* are ultimately implemented with webkitGenerateKeyRequest in prefixed EME.
*
* @param {?BufferSource} initData
* @param {?string} offlineSessionId
* @return {!Promise}
* @private
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession.prototype.generate_ =
function(initData, offlineSessionId) {
if (this.initialized_) {
return Promise.reject(new Error('The session is already initialized.'));
}
this.initialized_ = true;
/** @type {!Uint8Array} */
var mangledInitData;
try {
if (this.type_ == 'persistent-license') {
var StringUtils = shaka.util.StringUtils;
if (!offlineSessionId) {
// Persisting the initial license.
// Prefix the init data with a tag to indicate persistence.
var prefix = StringUtils.toUTF8('PERSISTENT|');
var result = new Uint8Array(prefix.byteLength + initData.byteLength);
result.set(new Uint8Array(prefix), 0);
result.set(new Uint8Array(initData), prefix.byteLength);
mangledInitData = result;
} else {
// Loading a stored license.
// Prefix the init data (which is really a session ID) with a tag to
// indicate that we are loading a persisted session.
mangledInitData = new Uint8Array(
StringUtils.toUTF8('LOAD_SESSION|' + offlineSessionId));
}
} else {
// Streaming.
goog.asserts.assert(this.type_ == 'temporary',
'expected temporary session');
goog.asserts.assert(!offlineSessionId,
'unexpected offline session ID');
mangledInitData = new Uint8Array(initData);
}
goog.asserts.assert(mangledInitData,
'init data not set!');
} catch (exception) {
return Promise.reject(exception);
}
goog.asserts.assert(this.generatePromise_ == null,
'generatePromise_ should be null');
this.generatePromise_ = new shaka.util.PublicPromise();
// Because we are hacking media.src in createSession to better emulate
// unprefixed EME's ability to create sessions and license requests without a
// video tag, we can get ourselves into trouble. It seems that sometimes,
// the setting of media.src hasn't been processed by some other thread, and
// GKR can throw an exception. If this occurs, wait 10 ms and try again at
// most once. This situation should only occur when init data is available
// ahead of the 'needkey' event.
var prefixApi = shaka.polyfill.PatchedMediaKeysWebkit.prefixApi_;
var generateKeyRequestName = prefixApi('generateKeyRequest');
try {
this.media_[generateKeyRequestName](this.keySystem_, mangledInitData);
} catch (exception) {
if (exception.name != 'InvalidStateError') {
this.generatePromise_ = null;
return Promise.reject(exception);
}
setTimeout(function() {
try {
this.media_[generateKeyRequestName](this.keySystem_, mangledInitData);
} catch (exception) {
this.generatePromise_.reject(exception);
this.generatePromise_ = null;
}
}.bind(this), 10);
}
return this.generatePromise_;
};
/**
* An internal version of update which defers new calls while old ones are in
* progress.
*
* @param {!shaka.util.PublicPromise} promise The promise associated with this
* call.
* @param {?BufferSource} response
* @private
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession.prototype.update_ =
function(promise, response) {
if (this.updatePromise_) {
// We already have an update in-progress, so defer this one until after the
// old one is resolved. Execute this whether the original one succeeds or
// fails.
this.updatePromise_.then(
this.update_.bind(this, promise, response)
).catch(
this.update_.bind(this, promise, response)
);
return;
}
this.updatePromise_ = promise;
var key;
var keyId;
if (this.keySystem_ == 'webkit-org.w3.clearkey') {
// The current EME version of clearkey wants a structured JSON response.
// The v0.1b version wants just a raw key. Parse the JSON response and
// extract the key and key ID.
var StringUtils = shaka.util.StringUtils;
var Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
var licenseString = StringUtils.fromUTF8(response);
var jwkSet = /** @type {JWKSet} */ (JSON.parse(licenseString));
var kty = jwkSet.keys[0].kty;
if (kty != 'oct') {
// Reject the promise.
var error = new Error('Response is not a valid JSON Web Key Set.');
this.updatePromise_.reject(error);
this.updatePromise_ = null;
}
key = Uint8ArrayUtils.fromBase64(jwkSet.keys[0].k);
keyId = Uint8ArrayUtils.fromBase64(jwkSet.keys[0].kid);
} else {
// The key ID is not required.
key = new Uint8Array(response);
keyId = null;
}
var prefixApi = shaka.polyfill.PatchedMediaKeysWebkit.prefixApi_;
var addKeyName = prefixApi('addKey');
try {
this.media_[addKeyName](this.keySystem_, key, keyId, this.sessionId);
} catch (exception) {
// Reject the promise.
this.updatePromise_.reject(exception);
this.updatePromise_ = null;
}
};
/**
* Update key status and dispatch a 'keystatuseschange' event.
*
* @param {string} status
* @private
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession.prototype.
updateKeyStatus_ = function(status) {
this.keyStatuses.setStatus(status);
var event = new shaka.util.FakeEvent('keystatuseschange');
this.dispatchEvent(event);
};
/** @override */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession.prototype.
generateRequest = function(initDataType, initData) {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.generateRequest');
return this.generate_(initData, null);
};
/** @override */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession.prototype.load =
function(sessionId) {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.load');
if (this.type_ == 'persistent-license') {
return this.generate_(null, sessionId);
} else {
return Promise.reject(new Error('Not a persistent session.'));
}
};
/** @override */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession.prototype.update =
function(response) {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.update', response);
goog.asserts.assert(this.sessionId, 'update without session ID');
var nextUpdatePromise = new shaka.util.PublicPromise();
this.update_(nextUpdatePromise, response);
return nextUpdatePromise;
};
/** @override */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession.prototype.close =
function() {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.close');
// This will remove a persistent session, but it's also the only way to
// free CDM resources on v0.1b.
if (this.type_ != 'persistent-license') {
// sessionId may reasonably be null if no key request has been generated
// yet. Unprefixed EME will return a rejected promise in this case.
// We will use the same error message that Chrome 41 uses in its EME
// implementation.
if (!this.sessionId) {
this.closed.reject(new Error('The session is not callable.'));
return this.closed;
}
// This may throw an exception, but we ignore it because we are only using
// it to clean up resources in v0.1b. We still consider the session closed.
// We can't let the exception propagate because MediaKeySession.close()
// should not throw.
var prefixApi = shaka.polyfill.PatchedMediaKeysWebkit.prefixApi_;
var cancelKeyRequestName = prefixApi('cancelKeyRequest');
try {
this.media_[cancelKeyRequestName](this.keySystem_, this.sessionId);
} catch (exception) {}
}
// Resolve the 'closed' promise and return it.
this.closed.resolve();
return this.closed;
};
/** @override */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeySession.prototype.remove =
function() {
shaka.log.debug('PatchedMediaKeysWebkit.MediaKeySession.remove');
if (this.type_ != 'persistent-license') {
return Promise.reject(new Error('Not a persistent session.'));
}
return this.close();
};
/**
* An implementation of MediaKeyStatusMap.
* This fakes a map with a single key ID.
*
* @constructor
* @struct
* @implements {MediaKeyStatusMap}
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap = function() {
/**
* @type {number}
*/
this.size = 0;
/**
* @private {string|undefined}
*/
this.status_ = undefined;
};
/**
* @const {!ArrayBuffer}
* @private
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap.KEY_ID_;
/**
* An internal method used by the session to set key status.
* @param {string|undefined} status
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap.prototype.setStatus =
function(status) {
this.size = status == undefined ? 0 : 1;
this.status_ = status;
};
/**
* An internal method used by the session to get key status.
* @return {string|undefined}
*/
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap.prototype.getStatus =
function() {
return this.status_;
};
/** @override */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap.prototype.forEach =
function(fn) {
if (this.status_) {
var fakeKeyId =
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap.KEY_ID_;
fn(this.status_, fakeKeyId);
}
};
/** @override */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap.prototype.get =
function(keyId) {
if (this.has(keyId)) {
return this.status_;
}
return undefined;
};
/** @override */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap.prototype.has =
function(keyId) {
var fakeKeyId =
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap.KEY_ID_;
if (this.status_ &&
shaka.util.Uint8ArrayUtils.equal(
new Uint8Array(keyId), new Uint8Array(fakeKeyId))) {
return true;
}
return false;
};
/** @suppress {missingReturn} */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap.prototype.
entries = function() {
goog.asserts.assert(false, 'Not used! Provided only for compiler.');
};
/** @suppress {missingReturn} */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap.prototype.
keys = function() {
goog.asserts.assert(false, 'Not used! Provided only for compiler.');
};
/** @suppress {missingReturn} */
shaka.polyfill.PatchedMediaKeysWebkit.MediaKeyStatusMap.prototype.
values = function() {
goog.asserts.assert(false, 'Not used! Provided only for compiler.');
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.polyfill.Promise');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.polyfill.register');
/**
* @summary A polyfill to implement Promises, primarily for IE.
* Only partially supports thenables, but otherwise passes the A+ conformance
* tests.
* Note that Promise.all() and Promise.race() are not tested by that suite.
*
* @constructor
* @struct
* @param {function(function(*), function(*))=} opt_callback
* @template T
*/
shaka.polyfill.Promise = function(opt_callback) {
/** @private {!Array.<shaka.polyfill.Promise.Child>} */
this.thens_ = [];
/** @private {!Array.<shaka.polyfill.Promise.Child>} */
this.catches_ = [];
/** @private {shaka.polyfill.Promise.State} */
this.state_ = shaka.polyfill.Promise.State.PENDING;
/** @private {*} */
this.value_;
// External callers must supply the callback. Internally, we may construct
// child Promises without it, since we can directly access their resolve_ and
// reject_ methods when convenient.
if (opt_callback) {
try {
opt_callback(this.resolve_.bind(this), this.reject_.bind(this));
} catch (e) {
this.reject_(e);
}
}
};
/**
* @typedef {{
* promise: !shaka.polyfill.Promise,
* callback: (function(*)|undefined)
* }}
*
* @summary A child promise, used for chaining.
* @description
* Only exists in the context of a then or catch chain.
* @property {!shaka.polyfill.Promise} promise
* The child promise.
* @property {(function(*)|undefined)} callback
* The then or catch callback to be invoked as part of this chain.
*/
shaka.polyfill.Promise.Child;
/**
* @enum {number}
*/
shaka.polyfill.Promise.State = {
PENDING: 0,
RESOLVED: 1,
REJECTED: 2
};
/**
* Install the polyfill if needed.
* @param {boolean=} opt_force If true, force the polyfill to be installed.
* Used in some unit tests.
*/
shaka.polyfill.Promise.install = function(opt_force) {
// Decide on the best way to invoke a callback as soon as possible.
// Precompute the setImmediate/clearImmediate convenience methods to avoid the
// overhead of this switch every time a callback has to be invoked.
if (window.setImmediate) {
// For IE and node.js:
shaka.polyfill.Promise.setImmediate_ = function(callback) {
return window.setImmediate(callback);
};
shaka.polyfill.Promise.clearImmediate_ = function(id) {
return window.clearImmediate(id);
};
} else {
// For everyone else:
shaka.polyfill.Promise.setImmediate_ = function(callback) {
return window.setTimeout(callback, 0);
};
shaka.polyfill.Promise.clearImmediate_ = function(id) {
return window.clearTimeout(id);
};
}
if (window.Promise && !opt_force) {
shaka.log.info('Using native Promises.');
return;
}
shaka.log.info('Using Promises polyfill.');
// Quoted to work around type-checking, since our then() signature doesn't
// exactly match that of a native Promise.
window['Promise'] = shaka.polyfill.Promise;
// Explicitly installed because the compiler won't necessarily attach them
// to the compiled constructor. Exporting them will only attach them to
// their original namespace, which isn't the same as attaching them to the
// constructor unless you also export the constructor.
window['Promise'].resolve = shaka.polyfill.Promise.resolve;
window['Promise'].reject = shaka.polyfill.Promise.reject;
window['Promise'].all = shaka.polyfill.Promise.all;
window['Promise'].race = shaka.polyfill.Promise.race;
// These are manually exported as well, because allowing the compiler to
// export them for us will cause the polyfill to end up in our generated
// externs. Since nobody should be accessing this directly using the
// shaka.polyfill namespace, it is okay not to @export these methods.
window['Promise']['prototype']['then'] =
shaka.polyfill.Promise.prototype.then;
window['Promise']['prototype']['catch'] =
shaka.polyfill.Promise.prototype.catch;
};
/**
* Uninstall the polyfill. Used in some unit tests.
*/
shaka.polyfill.Promise.uninstall = function() {
// Do nothing if there is no native implementation.
if (shaka.polyfill.Promise.nativePromise_) {
shaka.log.info('Removing Promise polyfill.');
window['Promise'] = shaka.polyfill.Promise.nativePromise_;
shaka.polyfill.Promise.q_ = [];
}
};
/**
* @param {*} value
* @return {!shaka.polyfill.Promise}
*/
shaka.polyfill.Promise.resolve = function(value) {
var p = new shaka.polyfill.Promise();
p.resolve_(undefined);
return p.then(function() {
return value;
});
};
/**
* @param {*} reason
* @return {!shaka.polyfill.Promise}
*/
shaka.polyfill.Promise.reject = function(reason) {
var p = new shaka.polyfill.Promise();
p.reject_(reason);
return p;
};
/**
* @param {!Array.<!shaka.polyfill.Promise>} others
* @return {!shaka.polyfill.Promise}
*/
shaka.polyfill.Promise.all = function(others) {
var p = new shaka.polyfill.Promise();
if (!others.length) {
p.resolve_([]);
return p;
}
// The array of results must be in the same order as the array of Promises
// passed to all(). So we pre-allocate the array and keep a count of how
// many have resolved. Only when all have resolved is the returned Promise
// itself resolved.
var count = 0;
var values = new Array(others.length);
var resolve = function(p, i, newValue) {
goog.asserts.assert(p.state_ != shaka.polyfill.Promise.State.RESOLVED,
'Invalid Promise state in Promise.all');
// If one of the Promises in the array was rejected, this Promise was
// rejected and new values are ignored. In such a case, the values array
// and its contents continue to be alive in memory until all of the Promises
// in the array have completed.
if (p.state_ == shaka.polyfill.Promise.State.PENDING) {
values[i] = newValue;
count++;
if (count == values.length) {
p.resolve_(values);
}
}
};
var reject = p.reject_.bind(p);
for (var i = 0; i < others.length; ++i) {
if (others[i] && others[i].then) {
others[i].then(resolve.bind(null, p, i), reject);
} else {
resolve(p, i, others[i]);
}
}
return p;
};
/**
* @param {!Array.<!shaka.polyfill.Promise>} others
* @return {!shaka.polyfill.Promise}
*/
shaka.polyfill.Promise.race = function(others) {
var p = new shaka.polyfill.Promise();
// The returned Promise is resolved or rejected as soon as one of the others
// is.
var resolve = p.resolve_.bind(p);
var reject = p.reject_.bind(p);
for (var i = 0; i < others.length; ++i) {
if (others[i] && others[i].then) {
others[i].then(resolve, reject);
} else {
resolve(others[i]);
}
}
return p;
};
/**
* @param {function(*)=} opt_successCallback
* @param {function(*)=} opt_failCallback
* @return {!shaka.polyfill.Promise}
*/
shaka.polyfill.Promise.prototype.then = function(opt_successCallback,
opt_failCallback) {
// then() returns a child Promise which is chained onto this one.
var child = new shaka.polyfill.Promise();
switch (this.state_) {
case shaka.polyfill.Promise.State.RESOLVED:
// This is already resolved, so we can chain to the child ASAP.
this.schedule_(child, opt_successCallback);
break;
case shaka.polyfill.Promise.State.REJECTED:
// This is already rejected, so we can chain to the child ASAP.
this.schedule_(child, opt_failCallback);
break;
case shaka.polyfill.Promise.State.PENDING:
// This is pending, so we have to track both callbacks and the child
// in order to chain later.
this.thens_.push({ promise: child, callback: opt_successCallback});
this.catches_.push({ promise: child, callback: opt_failCallback});
break;
}
return child;
};
/**
* @param {function(*)=} opt_callback
* @return {!shaka.polyfill.Promise}
*/
shaka.polyfill.Promise.prototype.catch = function(opt_callback) {
// Devolves into a two-argument call to 'then'.
return this.then(undefined, opt_callback);
};
/**
* @param {*} value
* @private
*/
shaka.polyfill.Promise.prototype.resolve_ = function(value) {
// Ignore resolve calls if we aren't still pending.
if (this.state_ == shaka.polyfill.Promise.State.PENDING) {
this.value_ = value;
this.state_ = shaka.polyfill.Promise.State.RESOLVED;
// Schedule calls to all of the chained callbacks.
for (var i = 0; i < this.thens_.length; ++i) {
this.schedule_(this.thens_[i].promise, this.thens_[i].callback);
}
this.thens_ = [];
this.catches_ = [];
}
};
/**
* @param {*} reason
* @private
*/
shaka.polyfill.Promise.prototype.reject_ = function(reason) {
// Ignore reject calls if we aren't still pending.
if (this.state_ == shaka.polyfill.Promise.State.PENDING) {
this.value_ = reason;
this.state_ = shaka.polyfill.Promise.State.REJECTED;
// Schedule calls to all of the chained callbacks.
for (var i = 0; i < this.catches_.length; ++i) {
this.schedule_(this.catches_[i].promise, this.catches_[i].callback);
}
this.thens_ = [];
this.catches_ = [];
}
};
/**
* @param {!shaka.polyfill.Promise} child
* @param {function(*)|undefined} callback
* @private
*/
shaka.polyfill.Promise.prototype.schedule_ = function(child, callback) {
goog.asserts.assert(this.state_ != shaka.polyfill.Promise.State.PENDING,
'Invalid Promise state in Promise.schedule_');
var Promise = shaka.polyfill.Promise;
var wrapper = function() {
if (callback && typeof callback == 'function') {
// Wrap around the callback. Exceptions thrown by the callback are
// converted to failures.
try {
var value = callback(this.value_);
} catch (exception) {
child.reject_(exception);
return;
}
// According to the spec, 'then' in a thenable may only be accessed once
// and any thrown exceptions in the getter must cause the Promise chain
// to fail.
var then;
try {
then = value && value.then;
} catch (exception) {
child.reject_(exception);
return;
}
if (value instanceof Promise) {
// If the returned value is a Promise, we bind it's state to the child.
if (value == child) {
// Without this, a bad calling pattern can cause an infinite loop.
child.reject_(new TypeError('Chaining cycle detected'));
} else {
value.then(child.resolve_.bind(child), child.reject_.bind(child));
}
} else if (then) {
// If the returned value is thenable, chain it to the child.
Promise.handleThenable_(value, then, child);
} else {
// If the returned value is not a Promise, the child is resolved with
// that value.
child.resolve_(value);
}
} else if (this.state_ == Promise.State.RESOLVED) {
// No callback for this state, so just chain on down the line.
child.resolve_(this.value_);
} else {
// No callback for this state, so just chain on down the line.
child.reject_(this.value_);
}
};
// Enqueue a call to the wrapper.
Promise.q_.push(wrapper.bind(this));
if (Promise.flushTimer_ == null) {
Promise.flushTimer_ = Promise.setImmediate_(Promise.flush);
}
};
/**
* @param {!Object} thenable
* @param {Function} then
* @param {!shaka.polyfill.Promise} child
* @private
*/
shaka.polyfill.Promise.handleThenable_ = function(thenable, then, child) {
var Promise = shaka.polyfill.Promise;
try {
var sealed = false;
then.call(thenable, function(value) {
if (sealed) return;
sealed = true;
var nextThen;
try {
nextThen = value && value.then;
} catch (exception) {
child.reject_(exception);
return;
}
if (nextThen) {
Promise.handleThenable_(value, nextThen, child);
} else {
child.resolve_(value);
}
}, child.reject_.bind(child));
} catch (exception) {
child.reject_(exception);
}
};
/**
* Flush the queue of callbacks.
* Used directly by some unit tests.
*/
shaka.polyfill.Promise.flush = function() {
var Promise = shaka.polyfill.Promise;
// Flush as long as we have callbacks. This means we can finish a chain more
// quickly, since we avoid the overhead of multiple calls to setTimeout, each
// of which has a minimum resolution of as much as 15ms on IE11.
// This helps to fix the out-of-order task bug on IE:
// https://github.com/google/shaka-player/issues/251#issuecomment-178146242
while (Promise.q_.length) {
// Callbacks may enqueue other callbacks, so clear the timer ID and swap the
// queue before we do anything else.
if (Promise.flushTimer_ != null) {
Promise.clearImmediate_(Promise.flushTimer_);
Promise.flushTimer_ = null;
}
var q = Promise.q_;
Promise.q_ = [];
for (var i = 0; i < q.length; ++i) {
q[i]();
}
}
};
/**
* @param {function()} callback
* @return {number}
* Schedule a callback as soon as possible.
* Bound in shaka.polyfill.Promise.install() to a specific implementation.
* @private
*/
shaka.polyfill.Promise.setImmediate_ = function(callback) { return 0; };
/**
* @param {number} id
* Clear a scheduled callback.
* Bound in shaka.polyfill.Promise.install() to a specific implementation.
* @private
*/
shaka.polyfill.Promise.clearImmediate_ = function(id) {};
/**
* A timer ID to flush the queue.
* @private {?number}
*/
shaka.polyfill.Promise.flushTimer_ = null;
/**
* A queue of callbacks to be invoked ASAP in the next frame.
* @private {!Array.<function()>}
*/
shaka.polyfill.Promise.q_ = [];
/** @private {?} */
shaka.polyfill.Promise.nativePromise_ = window.Promise;
shaka.polyfill.register(shaka.polyfill.Promise.install);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.polyfill.VideoPlaybackQuality');
goog.require('shaka.polyfill.register');
/**
* @namespace shaka.polyfill.VideoPlaybackQuality
*
* @summary A polyfill to provide MSE VideoPlaybackQuality metrics.
* Many browsers do not yet provide this API, and Chrome currently provides
* similar data through individual prefixed attributes on HTMLVideoElement.
*/
/**
* Install the polyfill if needed.
*/
shaka.polyfill.VideoPlaybackQuality.install = function() {
if (!window.HTMLVideoElement) {
// Avoid errors on very old browsers.
return;
}
var proto = HTMLVideoElement.prototype;
if (proto.getVideoPlaybackQuality) {
// No polyfill needed.
return;
}
if ('webkitDroppedFrameCount' in proto) {
proto.getVideoPlaybackQuality =
shaka.polyfill.VideoPlaybackQuality.webkit_;
}
};
/**
* @this {HTMLVideoElement}
* @return {!VideoPlaybackQuality}
* @private
*/
shaka.polyfill.VideoPlaybackQuality.webkit_ = function() {
return {
'droppedVideoFrames': this.webkitDroppedFrameCount,
'totalVideoFrames': this.webkitDecodedFrameCount,
// Not provided by this polyfill:
'corruptedVideoFrames': 0,
'creationTime': NaN,
'totalFrameDelay': 0
};
};
shaka.polyfill.register(shaka.polyfill.VideoPlaybackQuality.install);
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.polyfill.VTTCue');
goog.require('shaka.log');
goog.require('shaka.polyfill.register');
/**
* @namespace shaka.polyfill.VTTCue
*
* @summary A polyfill to provide VTTCue.
*/
/**
* Install the polyfill if needed.
*/
shaka.polyfill.VTTCue.install = function() {
if (window.VTTCue) {
shaka.log.info('Using native VTTCue.');
return;
}
if (!window.TextTrackCue) {
shaka.log.error('VTTCue not available.');
return;
}
var constructorLength = TextTrackCue.length;
if (constructorLength == 3) {
shaka.log.info('Using VTTCue polyfill from 3 argument TextTrackCue.');
window.VTTCue = shaka.polyfill.VTTCue.from3ArgsTextTrackCue_;
} else if (constructorLength == 6) {
shaka.log.info('Using VTTCue polyfill from 6 argument TextTrackCue.');
window.VTTCue = shaka.polyfill.VTTCue.from6ArgsTextTrackCue_;
} else if (shaka.polyfill.VTTCue.canUse3ArgsTextTrackCue_()) {
shaka.log.info('Using VTTCue polyfill from 3 argument TextTrackCue.');
window.VTTCue = shaka.polyfill.VTTCue.from3ArgsTextTrackCue_;
}
};
/**
* Draft spec TextTrackCue with 3 constructor arguments.
* See {@link https://goo.gl/ZXBWZi W3C Working Draft 25 October 2012}.
*
* @param {number} startTime
* @param {number} endTime
* @param {string} text
* @return {TextTrackCue}
* @private
*/
shaka.polyfill.VTTCue.from3ArgsTextTrackCue_ = function(startTime, endTime,
text) {
return new window.TextTrackCue(startTime, endTime, text);
};
/**
* Draft spec TextTrackCue with 6 constructor arguments (5th & 6th are
* optional).
* See {@link https://goo.gl/AYFqUh W3C Working Draft 29 March 2012}.
* Quoting the access to the TextTrackCue object to avoid the compiler
* complaining.
*
* @param {number} startTime
* @param {number} endTime
* @param {string} text
* @return {TextTrackCue}
* @private
*/
shaka.polyfill.VTTCue.from6ArgsTextTrackCue_ = function(startTime, endTime,
text) {
var id = startTime + '-' + endTime + '-' + text;
return new window['TextTrackCue'](id, startTime, endTime, text);
};
/**
* IE10, IE11 and Edge returns TextTrackCue.length = 0 although it accepts 3
* constructor arguments.
*
* @return {boolean}
* @private
*/
shaka.polyfill.VTTCue.canUse3ArgsTextTrackCue_ = function() {
try {
return !!shaka.polyfill.VTTCue.from3ArgsTextTrackCue_(1, 2, '');
} catch (error) {
return false;
}
};
shaka.polyfill.register(shaka.polyfill.VTTCue.install);
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| array_utils.js | 5.88% | (1 / 17) | 0% | (0 / 8) | 0% | (0 / 2) | 6.25% | (1 / 16) | |
| cancelable_chain.js | 2.86% | (1 / 35) | 0% | (0 / 6) | 0% | (0 / 8) | 2.94% | (1 / 34) | |
| config_utils.js | 3.85% | (1 / 26) | 0% | (0 / 24) | 0% | (0 / 1) | 3.85% | (1 / 26) | |
| data_view_reader.js | 1.39% | (1 / 72) | 0% | (0 / 10) | 0% | (0 / 12) | 1.41% | (1 / 71) | |
| ebml_parser.js | 1.19% | (1 / 84) | 0% | (0 / 28) | 0% | (0 / 12) | 1.19% | (1 / 84) | |
| error.js | 4% | (1 / 25) | 0% | (0 / 6) | 0% | (0 / 2) | 4% | (1 / 25) | |
| event_manager.js | 2.78% | (1 / 36) | 0% | (0 / 6) | 0% | (0 / 7) | 2.78% | (1 / 36) | |
| fake_event.js | 5.56% | (1 / 18) | 0% | (0 / 6) | 0% | (0 / 4) | 5.56% | (1 / 18) | |
| fake_event_target.js | 3.57% | (1 / 28) | 0% | (0 / 6) | 0% | (0 / 4) | 3.57% | (1 / 28) | |
| functional.js | 5.88% | (1 / 17) | 100% | (0 / 0) | 0% | (0 / 10) | 6.67% | (1 / 15) | |
| i_destroyable.js | 33.33% | (1 / 3) | 100% | (0 / 0) | 0% | (0 / 2) | 33.33% | (1 / 3) | |
| language_utils.js | 4.55% | (1 / 22) | 0% | (0 / 12) | 0% | (0 / 2) | 4.55% | (1 / 22) | |
| map_utils.js | 4.55% | (1 / 22) | 0% | (0 / 4) | 0% | (0 / 11) | 4.76% | (1 / 21) | |
| mp4_parser.js | 2.7% | (1 / 37) | 0% | (0 / 8) | 0% | (0 / 2) | 2.7% | (1 / 37) | |
| multi_map.js | 2.94% | (1 / 34) | 0% | (0 / 8) | 0% | (0 / 9) | 3.03% | (1 / 33) | |
| pssh.js | 2.44% | (1 / 41) | 0% | (0 / 10) | 0% | (0 / 1) | 2.44% | (1 / 41) | |
| public_promise.js | 8.33% | (1 / 12) | 100% | (0 / 0) | 0% | (0 / 2) | 8.33% | (1 / 12) | |
| stream_utils.js | 0.71% | (1 / 140) | 0% | (0 / 113) | 0% | (0 / 23) | 0.73% | (1 / 137) | |
| string_utils.js | 1.54% | (1 / 65) | 0% | (0 / 43) | 0% | (0 / 6) | 1.59% | (1 / 63) | |
| text_parser.js | 3.03% | (1 / 33) | 0% | (0 / 11) | 0% | (0 / 8) | 3.03% | (1 / 33) | |
| timer.js | 5.56% | (1 / 18) | 0% | (0 / 4) | 0% | (0 / 5) | 5.56% | (1 / 18) | |
| uint8array_utils.js | 2.78% | (1 / 36) | 0% | (0 / 18) | 0% | (0 / 5) | 3.23% | (1 / 31) | |
| xml_utils.js | 1.32% | (1 / 76) | 0% | (0 / 58) | 0% | (0 / 13) | 1.32% | (1 / 76) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.ArrayUtils');
/**
* @namespace shaka.util.ArrayUtils
* @summary Array utility functions.
*/
/**
* Remove duplicate entries from an array. Order N^2, so use with caution.
* @param {!Array.<T>} array
* @param {function(T, T): boolean=} opt_compareFn An optional function which
* will be used to compare items in the array.
* @return {!Array.<T>}
* @template T
*/
shaka.util.ArrayUtils.removeDuplicates = function(array, opt_compareFn) {
var result = [];
for (var i = 0; i < array.length; ++i) {
var matchFound = false;
for (var j = 0; j < result.length; ++j) {
matchFound = opt_compareFn ? opt_compareFn(array[i], result[j]) :
array[i] === result[j];
if (matchFound) break;
}
if (!matchFound) {
result.push(array[i]);
}
}
return result;
};
/**
* Find an item in an array. For use when comparison of entries via == will
* not suffice.
* @param {!Array.<T>} array
* @param {T} value
* @param {function(T, T): boolean} compareFn A function which will be used to
* compare items in the array.
* @return {number} The index, or -1 if not found.
* @template T
*/
shaka.util.ArrayUtils.indexOf = function(array, value, compareFn) {
for (var i = 0; i < array.length; ++i) {
if (compareFn(array[i], value)) {
return i;
}
}
return -1;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.CancelableChain');
goog.require('goog.asserts');
goog.require('shaka.util.Error');
/**
* A Promise-based abstraction that creates cancelable Promise chains.
* When canceled, subsequent stages of the internal Promise chain will stop.
* A canceled chain is rejected with a user-specified value.
*
* A CancelableChain only supports linear Promise chains. Chains which branch
* (more than one then() handler chained to a particular stage) are not
* supported. You will not be prevented from treating this as if branching
* were supported, but everything will be serialized into a linear chain.
* Be careful!
*
* @constructor
* @struct
*/
shaka.util.CancelableChain = function() {
/** @private {!Promise} */
this.promise_ = Promise.resolve();
/** @private {boolean} */
this.final_ = false;
/** @private {boolean} */
this.complete_ = false;
/** @private {boolean} */
this.canceled_ = false;
/** @private {shaka.util.Error} */
this.rejectionValue_;
/** @private {function()} */
this.onCancelComplete_;
/** @private {!Promise} */
this.cancelPromise_ = new Promise(function(resolve) {
this.onCancelComplete_ = resolve;
}.bind(this));
};
/**
* @param {function(*)} callback
* @return {!shaka.util.CancelableChain} the chain itself.
*/
shaka.util.CancelableChain.prototype.then = function(callback) {
goog.asserts.assert(!this.final_, 'Chain should not be final!');
this.promise_ = this.promise_.then(callback).then(function(data) {
if (this.canceled_) {
this.onCancelComplete_();
return Promise.reject(this.rejectionValue_);
}
return Promise.resolve(data);
}.bind(this));
return this;
};
/**
* Finalize the chain.
* Converts the chain into a simple Promise and stops accepting new stages.
*
* @return {!Promise}
*/
shaka.util.CancelableChain.prototype.finalize = function() {
if (!this.final_) {
this.promise_ = this.promise_.then(function(data) {
this.complete_ = true;
return Promise.resolve(data);
}.bind(this), function(error) {
this.complete_ = true;
return Promise.reject(error);
}.bind(this));
}
this.final_ = true;
return this.promise_;
};
/**
* Cancel the Promise chain and reject with the given value.
*
* @param {!shaka.util.Error} reason
* @return {!Promise} resolved when the cancelation has been processed by the
* the chain and no more stages will execute. Note that this may be before
* the owner of the finalized chain has seen the rejection.
*/
shaka.util.CancelableChain.prototype.cancel = function(reason) {
if (this.complete_) return Promise.resolve();
this.canceled_ = true;
this.rejectionValue_ = reason;
return this.cancelPromise_;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.ConfigUtils');
goog.require('goog.asserts');
goog.require('shaka.log');
/**
* @param {!Object} destination
* @param {!Object} source
* @param {!Object} template supplies default values
* @param {!Object} overrides
* Supplies override type checking. When the current path matches the key in
* this object, each sub-value must match the type in this object. If this
* contains an Object, it is used as the template.
* @param {string} path to this part of the config
*/
shaka.util.ConfigUtils.mergeConfigObjects =
function(destination, source, template, overrides, path) {
goog.asserts.assert(destination, 'Destination config must not be null!');
/**
* @type {boolean}
* If true, don't validate the keys in the next level.
*/
var ignoreKeys = path in overrides;
for (var k in source) {
var subPath = path + '.' + k;
var subTemplate = ignoreKeys ? overrides[path] : template[k];
/**
* @type {boolean}
* If true, simply copy the object over and don't verify.
*/
var copyObject = !!({
'.abr.manager': true
})[subPath];
// The order of these checks is important.
if (!ignoreKeys && !(k in destination)) {
shaka.log.error('Invalid config, unrecognized key ' + subPath);
} else if (source[k] === undefined) {
// An explicit 'undefined' value causes the key to be deleted from the
// destination config and replaced with a default from the template if
// possible.
if (subTemplate === undefined || ignoreKeys) {
delete destination[k];
} else {
destination[k] = subTemplate;
}
} else if (copyObject) {
destination[k] = source[k];
} else if (typeof destination[k] == 'object' &&
typeof source[k] == 'object') {
shaka.util.ConfigUtils.mergeConfigObjects(
destination[k], source[k], subTemplate, overrides, subPath);
} else if (typeof source[k] != typeof subTemplate) {
shaka.log.error('Invalid config, wrong type for ' + subPath);
} else if (typeof destination[k] == 'function' &&
destination[k].length != source[k].length) {
shaka.log.warning(
'Invalid config, wrong number of arguments for ' + subPath);
destination[k] = source[k];
} else {
destination[k] = source[k];
}
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.DataViewReader');
goog.require('goog.asserts');
goog.require('shaka.util.Error');
goog.require('shaka.util.StringUtils');
/**
* Creates a DataViewReader, which abstracts a DataView object.
*
* @param {!DataView} dataView The DataView.
* @param {shaka.util.DataViewReader.Endianness} endianness The endianness.
*
* @struct
* @constructor
*/
shaka.util.DataViewReader = function(dataView, endianness) {
/** @private {!DataView} */
this.dataView_ = dataView;
/** @private {boolean} */
this.littleEndian_ =
endianness == shaka.util.DataViewReader.Endianness.LITTLE_ENDIAN;
/** @private {number} */
this.position_ = 0;
};
/**
* Endianness.
* @enum {number}
*/
shaka.util.DataViewReader.Endianness = {
BIG_ENDIAN: 0,
LITTLE_ENDIAN: 1
};
/**
* @return {boolean} True if the reader has more data, false otherwise.
*/
shaka.util.DataViewReader.prototype.hasMoreData = function() {
return this.position_ < this.dataView_.byteLength;
};
/**
* Gets the current byte position.
* @return {number}
*/
shaka.util.DataViewReader.prototype.getPosition = function() {
return this.position_;
};
/**
* Gets the byte length of the DataView.
* @return {number}
*/
shaka.util.DataViewReader.prototype.getLength = function() {
return this.dataView_.byteLength;
};
/**
* Reads an unsigned 8 bit integer, and advances the reader.
* @return {number} The integer.
* @throws {shaka.util.Error} when reading past the end of the data view.
*/
shaka.util.DataViewReader.prototype.readUint8 = function() {
try {
var value = this.dataView_.getUint8(this.position_);
} catch (exception) {
this.throwOutOfBounds_();
}
this.position_ += 1;
return value;
};
/**
* Reads an unsigned 16 bit integer, and advances the reader.
* @return {number} The integer.
* @throws {shaka.util.Error} when reading past the end of the data view.
*/
shaka.util.DataViewReader.prototype.readUint16 = function() {
try {
var value = this.dataView_.getUint16(this.position_, this.littleEndian_);
} catch (exception) {
this.throwOutOfBounds_();
}
this.position_ += 2;
return value;
};
/**
* Reads an unsigned 32 bit integer, and advances the reader.
* @return {number} The integer.
* @throws {shaka.util.Error} when reading past the end of the data view.
*/
shaka.util.DataViewReader.prototype.readUint32 = function() {
try {
var value = this.dataView_.getUint32(this.position_, this.littleEndian_);
} catch (exception) {
this.throwOutOfBounds_();
}
this.position_ += 4;
return value;
};
/**
* Reads an unsigned 64 bit integer, and advances the reader.
* @return {number} The integer.
* @throws {shaka.util.Error} when reading past the end of the data view or
* when reading an integer too large to store accurately in JavaScript.
*/
shaka.util.DataViewReader.prototype.readUint64 = function() {
var low, high;
try {
if (this.littleEndian_) {
low = this.dataView_.getUint32(this.position_, true);
high = this.dataView_.getUint32(this.position_ + 4, true);
} else {
high = this.dataView_.getUint32(this.position_, false);
low = this.dataView_.getUint32(this.position_ + 4, false);
}
} catch (exception) {
this.throwOutOfBounds_();
}
if (high > 0x1FFFFF) {
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.JS_INTEGER_OVERFLOW);
}
this.position_ += 8;
// NOTE: This is subtle, but in JavaScript you can't shift left by 32 and get
// the full range of 53-bit values possible. You must multiply by 2^32.
return (high * Math.pow(2, 32)) + low;
};
/**
* Reads the specified number of raw bytes.
* @param {number} bytes The number of bytes to read.
* @return {!Uint8Array}
* @throws {shaka.util.Error} when reading past the end of the data view.
*/
shaka.util.DataViewReader.prototype.readBytes = function(bytes) {
goog.asserts.assert(bytes > 0, 'Bad call to DataViewReader.readBytes');
if (this.position_ + bytes > this.dataView_.byteLength) {
this.throwOutOfBounds_();
}
var value = this.dataView_.buffer.slice(
this.position_, this.position_ + bytes);
this.position_ += bytes;
return new Uint8Array(value);
};
/**
* Skips the specified number of bytes.
* @param {number} bytes The number of bytes to skip.
* @throws {shaka.util.Error} when skipping past the end of the data view.
*/
shaka.util.DataViewReader.prototype.skip = function(bytes) {
goog.asserts.assert(bytes >= 0, 'Bad call to DataViewReader.skip');
if (this.position_ + bytes > this.dataView_.byteLength) {
this.throwOutOfBounds_();
}
this.position_ += bytes;
};
/**
* Keeps reading until it reaches a byte that equals to zero. The text is
* assumed to be UTF-8.
* @return {string}
* @throws {shaka.util.Error} when reading past the end of the data view.
*/
shaka.util.DataViewReader.prototype.readTerminatedString = function() {
var start = this.position_;
try {
while (this.hasMoreData()) {
var value = this.dataView_.getUint8(this.position_);
if (value == 0) break;
this.position_ += 1;
}
} catch (exception) {
this.throwOutOfBounds_();
}
var ret = this.dataView_.buffer.slice(start, this.position_);
// skip string termination
this.position_ += 1;
return shaka.util.StringUtils.fromUTF8(ret);
};
/**
* @throws {shaka.util.Error}
* @private
*/
shaka.util.DataViewReader.prototype.throwOutOfBounds_ = function() {
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.BUFFER_READ_OUT_OF_BOUNDS);
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.EbmlElement');
goog.provide('shaka.util.EbmlParser');
goog.require('shaka.util.DataViewReader');
goog.require('shaka.util.Error');
goog.require('shaka.util.Uint8ArrayUtils');
/**
* Creates an Extensible Binary Markup Language (EBML) parser.
* @param {!DataView} dataView The EBML data.
* @constructor
* @struct
*/
shaka.util.EbmlParser = function(dataView) {
/** @private {!DataView} */
this.dataView_ = dataView;
/** @private {!shaka.util.DataViewReader} */
this.reader_ = new shaka.util.DataViewReader(
dataView,
shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
// If not already constructed, build a list of EBML dynamic size constants.
// This is not done at load-time to avoid exceptions on unsupported browsers.
if (!shaka.util.EbmlParser.DYNAMIC_SIZES) {
shaka.util.EbmlParser.DYNAMIC_SIZES = [
new Uint8Array([0xff]),
new Uint8Array([0x7f, 0xff]),
new Uint8Array([0x3f, 0xff, 0xff]),
new Uint8Array([0x1f, 0xff, 0xff, 0xff]),
new Uint8Array([0x0f, 0xff, 0xff, 0xff, 0xff]),
new Uint8Array([0x07, 0xff, 0xff, 0xff, 0xff, 0xff]),
new Uint8Array([0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
new Uint8Array([0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
];
}
};
/** @const {!Array.<!Uint8Array>} */
shaka.util.EbmlParser.DYNAMIC_SIZES;
/**
* @return {boolean} True if the parser has more data, false otherwise.
*/
shaka.util.EbmlParser.prototype.hasMoreData = function() {
return this.reader_.hasMoreData();
};
/**
* Parses an EBML element from the parser's current position, and advances
* the parser.
* @return {!shaka.util.EbmlElement} The EBML element.
* @throws {shaka.util.Error}
* @see http://matroska.org/technical/specs/rfc/index.html
*/
shaka.util.EbmlParser.prototype.parseElement = function() {
var id = this.parseId_();
// Parse the element's size.
var vint = this.parseVint_();
var size;
if (shaka.util.EbmlParser.isDynamicSizeValue_(vint)) {
// If this has an unknown size, assume that it takes up the rest of the
// data.
size = this.dataView_.byteLength - this.reader_.getPosition();
} else {
size = shaka.util.EbmlParser.getVintValue_(vint);
}
// Note that if the element's size is larger than the buffer then we are
// parsing a "partial element". This may occur if for example we are
// parsing the beginning of some WebM container data, but our buffer does
// not contain the entire WebM container data.
var elementSize =
this.reader_.getPosition() + size <= this.dataView_.byteLength ?
size :
this.dataView_.byteLength - this.reader_.getPosition();
var dataView = new DataView(
this.dataView_.buffer,
this.dataView_.byteOffset + this.reader_.getPosition(), elementSize);
this.reader_.skip(elementSize);
return new shaka.util.EbmlElement(id, dataView);
};
/**
* Parses an EBML ID from the parser's current position, and advances the
* parser.
* @throws {shaka.util.Error}
* @return {number} The EBML ID.
* @private
*/
shaka.util.EbmlParser.prototype.parseId_ = function() {
var vint = this.parseVint_();
if (vint.length > 7) {
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.EBML_OVERFLOW);
}
var id = 0;
for (var i = 0; i < vint.length; i++) {
// Note that we cannot use << since |value| may exceed 32 bits.
id = (256 * id) + vint[i];
}
return id;
};
/**
* Parses a variable sized integer from the parser's current position, and
* advances the parser.
* For example:
* 1 byte wide: 1xxx xxxx
* 2 bytes wide: 01xx xxxx xxxx xxxx
* 3 bytes wide: 001x xxxx xxxx xxxx xxxx xxxx
* @throws {shaka.util.Error}
* @return {!Uint8Array} The variable sized integer.
* @private
*/
shaka.util.EbmlParser.prototype.parseVint_ = function() {
var firstByte = this.reader_.readUint8();
var numBytes;
// Determine the byte width of the variable sized integer.
for (numBytes = 1; numBytes <= 8; numBytes++) {
var mask = 0x1 << (8 - numBytes);
if (firstByte & mask) {
break;
}
}
if (numBytes > 8) {
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.EBML_OVERFLOW);
}
var vint = new Uint8Array(numBytes);
vint[0] = firstByte;
// Include the remaining bytes.
for (var i = 1; i < numBytes; i++) {
vint[i] = this.reader_.readUint8();
}
return vint;
};
/**
* Gets the value of a variable sized integer.
* For example, the x's below are part of the vint's value.
* 7-bit value: 1xxx xxxx
* 14-bit value: 01xx xxxx xxxx xxxx
* 21-bit value: 001x xxxx xxxx xxxx xxxx xxxx
* @param {!Uint8Array} vint The variable sized integer.
* @throws {shaka.util.Error}
* @return {number} The value of the variable sized integer.
* @private
*/
shaka.util.EbmlParser.getVintValue_ = function(vint) {
// If |vint| is 8 bytes wide then we must ensure that it does not have more
// than 53 meaningful bits. For example, assume |vint| is 8 bytes wide,
// so it has the following structure,
// 0000 0001 | xxxx xxxx ...
// Thus, the the first 3 bits following the first byte of |vint| must be 0.
if ((vint.length == 8) && (vint[1] & 0xe0)) {
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.JS_INTEGER_OVERFLOW);
}
// Mask out the first few bits of |vint|'s first byte to get the most
// significant bits of |vint|'s value. If |vint| is 8 bytes wide then |value|
// will be set to 0.
var mask = 0x1 << (8 - vint.length);
var value = vint[0] & (mask - 1);
// Add the remaining bytes.
for (var i = 1; i < vint.length; i++) {
// Note that we cannot use << since |value| may exceed 32 bits.
value = (256 * value) + vint[i];
}
return value;
};
/**
* Checks if the given variable sized integer represents a dynamic size value.
* @param {!Uint8Array} vint The variable sized integer.
* @return {boolean} true if |vint| represents a dynamic size value,
* false otherwise.
* @private
*/
shaka.util.EbmlParser.isDynamicSizeValue_ = function(vint) {
var EbmlParser = shaka.util.EbmlParser;
var uint8ArrayEqual = shaka.util.Uint8ArrayUtils.equal;
for (var i = 0; i < EbmlParser.DYNAMIC_SIZES.length; i++) {
if (uint8ArrayEqual(vint, EbmlParser.DYNAMIC_SIZES[i])) {
return true;
}
}
return false;
};
/**
* Creates an EbmlElement.
* @param {number} id The ID.
* @param {!DataView} dataView The DataView.
* @constructor
*/
shaka.util.EbmlElement = function(id, dataView) {
/** @type {number} */
this.id = id;
/** @private {!DataView} */
this.dataView_ = dataView;
};
/**
* Gets the element's offset from the beginning of the buffer.
* @return {number}
*/
shaka.util.EbmlElement.prototype.getOffset = function() {
return this.dataView_.byteOffset;
};
/**
* Interpret the element's data as a list of sub-elements.
* @throws {shaka.util.Error}
* @return {!shaka.util.EbmlParser} A parser over the sub-elements.
*/
shaka.util.EbmlElement.prototype.createParser = function() {
return new shaka.util.EbmlParser(this.dataView_);
};
/**
* Interpret the element's data as an unsigned integer.
* @throws {shaka.util.Error}
* @return {number}
*/
shaka.util.EbmlElement.prototype.getUint = function() {
if (this.dataView_.byteLength > 8) {
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.EBML_OVERFLOW);
}
// Ensure we have at most 53 meaningful bits.
if ((this.dataView_.byteLength == 8) && (this.dataView_.getUint8(0) & 0xe0)) {
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.JS_INTEGER_OVERFLOW);
}
var value = 0;
for (var i = 0; i < this.dataView_.byteLength; i++) {
var chunk = this.dataView_.getUint8(i);
value = (256 * value) + chunk;
}
return value;
};
/**
* Interpret the element's data as a floating point number (32 bits or 64 bits).
* 80-bit floating point numbers are not supported.
* @throws {shaka.util.Error}
* @return {number}
*/
shaka.util.EbmlElement.prototype.getFloat = function() {
if (this.dataView_.byteLength == 4) {
return this.dataView_.getFloat32(0);
} else if (this.dataView_.byteLength == 8) {
return this.dataView_.getFloat64(0);
} else {
throw new shaka.util.Error(
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.EBML_BAD_FLOATING_POINT_SIZE);
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.Error');
/**
* Creates a new Error.
*
* @param {shaka.util.Error.Category} category
* @param {shaka.util.Error.Code} code
* @param {...*} var_args
*
* @constructor
* @struct
* @export
* @extends {Error}
*/
shaka.util.Error = function(category, code, var_args) {
this.category = category;
this.code = code;
this.data = Array.prototype.slice.call(arguments, 2);
// This improves formatting of Errors in failure messages in the tests.
if (!COMPILED) {
var categoryName = 'UNKNOWN';
var codeName = 'UNKNOWN';
for (var k in shaka.util.Error.Category) {
if (shaka.util.Error.Category[k] == this.category)
categoryName = k;
}
for (var k in shaka.util.Error.Code) {
if (shaka.util.Error.Code[k] == this.code)
codeName = k;
}
/**
* A human-readable version of the category and code.
* <i>(Only available in uncompiled mode.)</i>
*
* @const {string}
* @exportDoc
*/
this.message = 'Shaka Error ' + categoryName + '.' + codeName +
' (' + this.data.toString() + ')';
try {
throw new Error(this.message);
} catch (e) {
/**
* A stack-trace showing where the error occurred.
* <i>(Only available in uncompiled mode.)</i>
*
* @const {string}
* @exportDoc
*/
this.stack = e.stack;
}
}
};
/**
* @const {shaka.util.Error.Category}
* @expose
*/
shaka.util.Error.prototype.category;
/**
* @const {shaka.util.Error.Code}
* @expose
*/
shaka.util.Error.prototype.code;
/**
* @const {!Array.<*>}
* @expose
*/
shaka.util.Error.prototype.data;
/**
* @return {string}
* @override
*/
shaka.util.Error.prototype.toString = function() {
return 'shaka.util.Error ' + JSON.stringify(this, null, ' ');
};
/**
* @enum {number}
* @export
*/
shaka.util.Error.Category = {
/** Errors from the network stack. */
'NETWORK': 1,
/** Errors parsing text streams. */
'TEXT': 2,
/** Errors parsing or processing audio or video streams. */
'MEDIA': 3,
/** Errors parsing the Manifest. */
'MANIFEST': 4,
/** Errors related to streaming. */
'STREAMING': 5,
/** Errors related to DRM. */
'DRM': 6,
/** Miscellaneous errors from the player. */
'PLAYER': 7,
/** Errors related to cast. */
'CAST': 8,
/** Errors in the database storage (offline). */
'STORAGE': 9
};
/**
* @enum {number}
* @export
*/
shaka.util.Error.Code = {
/**
* A network request was made using an unsupported URI scheme.
* <br> error.data[0] is the URI.
*/
'UNSUPPORTED_SCHEME': 1000,
/**
* An HTTP network request returned an HTTP status that indicated a failure.
* <br> error.data[0] is the URI.
* <br> error.data[1] is the status code.
* <br> error.data[2] is the response text, or null if the response could not
* be interpretted as text.
* <br> error.data[3] is the map of response headers.
*/
'BAD_HTTP_STATUS': 1001,
/**
* An HTTP network request failed with an error, but not from the server.
* <br> error.data[0] is the URI.
*/
'HTTP_ERROR': 1002,
/**
* A network request timed out.
* <br> error.data[0] is the URI.
*/
'TIMEOUT': 1003,
/**
* A network request was made with a malformed data URI.
* <br> error.data[0] is the URI.
*/
'MALFORMED_DATA_URI': 1004,
/**
* A network request was made with a data URI using an unknown encoding.
* <br> error.data[0] is the URI.
*/
'UNKNOWN_DATA_URI_ENCODING': 1005,
/** The text parser failed to parse a text stream due to an invalid header. */
'INVALID_TEXT_HEADER': 2000,
/** The text parser failed to parse a text stream due to an invalid cue. */
'INVALID_TEXT_CUE': 2001,
// RETIRED: 'INVALID_TEXT_SETTINGS': 2002,
/**
* Was unable to detect the encoding of the response text. Suggest adding
* byte-order-markings to the response data.
*/
'UNABLE_TO_DETECT_ENCODING': 2003,
/** The response data contains invalid Unicode character encoding. */
'BAD_ENCODING': 2004,
/**
* The XML parser failed to parse an xml stream.
*/
'INVALID_XML': 2005,
/**
* TTML stream lacks mandatory elements.
*/
'INVALID_TTML': 2006,
/**
* MP4 segment does not contain TTML.
*/
'INVALID_MP4_TTML': 2007,
/**
* MP4 segment does not contain VTT.
*/
'INVALID_MP4_VTT': 2008,
/**
* Some component tried to read past the end of a buffer. The segment index,
* init segment, or PSSH may be malformed.
*/
'BUFFER_READ_OUT_OF_BOUNDS': 3000,
/**
* Some component tried to parse an integer that was too large to fit in a
* JavaScript number without rounding error. JavaScript can only natively
* represent integers up to 53 bits.
*/
'JS_INTEGER_OVERFLOW': 3001,
/**
* The EBML parser used to parse the WebM container encountered an integer,
* ID, or other field larger than the maximum supported by the parser.
*/
'EBML_OVERFLOW': 3002,
/**
* The EBML parser used to parse the WebM container encountered a floating-
* point field of a size not supported by the parser.
*/
'EBML_BAD_FLOATING_POINT_SIZE': 3003,
/**
* The MP4 SIDX parser found the wrong box type.
* Either the segment index range is incorrect or the data is corrupt.
*/
'MP4_SIDX_WRONG_BOX_TYPE': 3004,
/**
* The MP4 SIDX parser encountered an invalid timescale.
* The segment index data may be corrupt.
*/
'MP4_SIDX_INVALID_TIMESCALE': 3005,
/** The MP4 SIDX parser encountered a type of SIDX that is not supported. */
'MP4_SIDX_TYPE_NOT_SUPPORTED': 3006,
/**
* The WebM Cues parser was unable to locate the Cues element.
* The segment index data may be corrupt.
*/
'WEBM_CUES_ELEMENT_MISSING': 3007,
/**
* The WebM header parser was unable to locate the Ebml element.
* The init segment data may be corrupt.
*/
'WEBM_EBML_HEADER_ELEMENT_MISSING': 3008,
/**
* The WebM header parser was unable to locate the Segment element.
* The init segment data may be corrupt.
*/
'WEBM_SEGMENT_ELEMENT_MISSING': 3009,
/**
* The WebM header parser was unable to locate the Info element.
* The init segment data may be corrupt.
*/
'WEBM_INFO_ELEMENT_MISSING': 3010,
/**
* The WebM header parser was unable to locate the Duration element.
* The init segment data may be corrupt or may have been incorrectly encoded.
* Shaka requires a duration in WebM DASH content.
*/
'WEBM_DURATION_ELEMENT_MISSING': 3011,
/**
* The WebM Cues parser was unable to locate the Cue Track Positions element.
* The segment index data may be corrupt.
*/
'WEBM_CUE_TRACK_POSITIONS_ELEMENT_MISSING': 3012,
/**
* The WebM Cues parser was unable to locate the Cue Time element.
* The segment index data may be corrupt.
*/
'WEBM_CUE_TIME_ELEMENT_MISSING': 3013,
/**
* A MediaSource operation failed.
* <br> error.data[0] is a MediaError code from the video element.
*/
'MEDIA_SOURCE_OPERATION_FAILED': 3014,
/**
* A MediaSource operation threw an exception.
* <br> error.data[0] is the exception that was thrown.
*/
'MEDIA_SOURCE_OPERATION_THREW': 3015,
/**
* The video element reported an error.
* <br> error.data[0] is a MediaError code from the video element.
* <br> On Edge & IE, error.data[1] is a Microsoft extended error code in hex.
*/
'VIDEO_ERROR': 3016,
/**
* A MediaSource operation threw QuotaExceededError and recovery failed. The
* content cannot be played correctly because the segments are too large for
* the browser/platform. This may occur when attempting to play very high
* quality, very high bitrate content on low-end devices.
* <br> error.data[0] is the type of content which caused the error.
*/
'QUOTA_EXCEEDED_ERROR': 3017,
/**
* The Player was unable to guess the manifest type based on file extension
* or MIME type. To fix, try one of the following:
* <br><ul>
* <li>Rename the manifest so that the URI ends in a well-known extension.
* <li>Configure the server to send a recognizable Content-Type header.
* <li>Configure the server to accept a HEAD request for the manifest.
* </ul>
* <br> error.data[0] is the manifest URI.
*/
'UNABLE_TO_GUESS_MANIFEST_TYPE': 4000,
/** The DASH Manifest contained invalid XML markup. */
'DASH_INVALID_XML': 4001,
/**
* The DASH Manifest contained a Representation with insufficient segment
* information.
*/
'DASH_NO_SEGMENT_INFO': 4002,
/** The DASH Manifest contained an AdaptationSet with no Representations. */
'DASH_EMPTY_ADAPTATION_SET': 4003,
/** The DASH Manifest contained an Period with no AdaptationSets. */
'DASH_EMPTY_PERIOD': 4004,
/**
* The DASH Manifest does not specify an init segment with a WebM container.
*/
'DASH_WEBM_MISSING_INIT': 4005,
/** The DASH Manifest contained an unsupported container format. */
'DASH_UNSUPPORTED_CONTAINER': 4006,
/** The embedded PSSH data has invalid encoding. */
'DASH_PSSH_BAD_ENCODING': 4007,
/**
* There is an AdaptationSet whose Representations do not have any common
* key-systems.
*/
'DASH_NO_COMMON_KEY_SYSTEM': 4008,
/** Having multiple key IDs per Representation is not supported. */
'DASH_MULTIPLE_KEY_IDS_NOT_SUPPORTED': 4009,
/** The DASH Manifest specifies conflicting key IDs. */
'DASH_CONFLICTING_KEY_IDS': 4010,
/**
* The manifest contains a period with no playable streams.
* Either the period was originally empty, or the streams within cannot be
* played on this browser or platform.
*/
'UNPLAYABLE_PERIOD': 4011,
/**
* There exist some streams that could be decoded, but restrictions imposed
* by the application or the key system prevent us from playing. This may
* happen under the following conditions:
* <ul>
* <li>The application has given restrictions to the Player that restrict
* at least one content type completely (e.g. no playable audio).
* <li>The key system has imposed output restrictions that cannot be met
* (such as HDCP) and there are no unrestricted alternatives.
* </ul>
*/
'RESTRICTIONS_CANNOT_BE_MET': 4012,
// RETIRED: 'INTERNAL_ERROR_KEY_STATUS': 4013,
/**
* No valid periods were found in the manifest. Please check that your
* manifest is correct and free of typos.
*/
'NO_PERIODS': 4014,
/**
* A Representation has an id that is the same as another Representation in
* the same Period. This makes manifest updates impossible since we cannot
* map the updated Representation to the old one.
*/
'DASH_DUPLICATE_REPRESENTATION_ID': 4018,
// RETIRED: 'INCONSISTENT_BUFFER_STATE': 5000,
// RETIRED: 'INVALID_SEGMENT_INDEX': 5001,
// RETIRED: 'SEGMENT_DOES_NOT_EXIST': 5002,
// RETIRED: 'CANNOT_SATISFY_BYTE_LIMIT': 5003,
// RETIRED: 'BAD_SEGMENT': 5004,
/**
* The StreamingEngine called onChooseStreams() but the callback receiver
* did not return the correct number or type of Streams.
*
* This can happen when there is multi-Period content where one Period is
* video+audio and another is video-only or audio-only. We don't support this
* case because it is incompatible with MSE. When the browser reaches the
* transition, it will pause, waiting for the audio stream.
*/
'INVALID_STREAMS_CHOSEN': 5005,
/**
* The manifest indicated protected content, but the manifest parser was
* unable to determine what key systems should be used.
*/
'NO_RECOGNIZED_KEY_SYSTEMS': 6000,
/**
* None of the requested key system configurations are available. This may
* happen under the following conditions:
* <ul>
* <li> The key system is not supported.
* <li> The key system does not support the features requested (e.g.
* persistent state).
* <li> A user prompt was shown and the user denied access.
* <li> The key system is not available from unsecure contexts. (ie.
requires HTTPS) See https://goo.gl/EEhZqT.
* </ul>
*/
'REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE': 6001,
/**
* The browser found one of the requested key systems, but it failed to
* create an instance of the CDM for some unknown reason.
* <br> error.data[0] is an error message string from the browser.
*/
'FAILED_TO_CREATE_CDM': 6002,
/**
* The browser found one of the requested key systems and created an instance
* of the CDM, but it failed to attach the CDM to the video for some unknown
* reason.
* <br> error.data[0] is an error message string from the browser.
*/
'FAILED_TO_ATTACH_TO_VIDEO': 6003,
/**
* The CDM rejected the server certificate supplied by the application.
* The certificate may be malformed or in an unsupported format.
* <br> error.data[0] is an error message string from the browser.
*/
'INVALID_SERVER_CERTIFICATE': 6004,
/**
* The CDM refused to create a session for some unknown reason.
* <br> error.data[0] is an error message string from the browser.
*/
'FAILED_TO_CREATE_SESSION': 6005,
/**
* The CDM was unable to generate a license request for the init data it was
* given. The init data may be malformed or in an unsupported format.
* <br> error.data[0] is an error message string from the browser.
*/
'FAILED_TO_GENERATE_LICENSE_REQUEST': 6006,
/**
* The license request failed. This could be a timeout, a network failure, or
* a rejection by the server.
* <br> error.data[0] is a shaka.util.Error from the networking engine.
*/
'LICENSE_REQUEST_FAILED': 6007,
/**
* The license response was rejected by the CDM. The server's response may be
* invalid or malformed for this CDM.
* <br> error.data[0] is an error message string from the browser.
*/
'LICENSE_RESPONSE_REJECTED': 6008,
// RETIRED: 'NO_LICENSE_SERVER_SPECIFIED': 6009,
/**
* The manifest does not specify any DRM info, but the content is encrypted.
* Either the manifest or the manifest parser are broken.
*/
'ENCRYPTED_CONTENT_WITHOUT_DRM_INFO': 6010,
// RETIRED: 'WRONG_KEYS': 6011,
/**
* No license server was given for the key system signaled by the manifest.
* A license server URI is required for every key system.
*/
'NO_LICENSE_SERVER_GIVEN': 6012,
/**
* A required offline session was removed. The content is not playable.
*/
'OFFLINE_SESSION_REMOVED': 6013,
/**
* The license has expired. This is triggered when playback is stalled on a
* 'waitingforkeys' event and there are any expired keys in the key status map
* of any active session.
*/
'EXPIRED': 6014,
/**
* The call to Player.load() was interrupted by a call to Player.unload()
* or another call to Player.load().
*/
'LOAD_INTERRUPTED': 7000,
/**
* The Cast API is unavailable. This may be because of one of the following:
* - The browser may not have Cast support
* - The browser may be missing a necessary Cast extension
* - The Cast sender library may not be loaded in your app
*/
'CAST_API_UNAVAILABLE': 8000,
/**
* No cast receivers are available at this time.
*/
'NO_CAST_RECEIVERS': 8001,
/**
* The library is already casting.
*/
'ALREADY_CASTING': 8002,
/**
* A Cast SDK error that we did not explicitly plan for has occurred.
* Check data[0] and refer to the Cast SDK documentation for details.
* <br> error.data[0] is an error object from the Cast SDK.
*/
'UNEXPECTED_CAST_ERROR': 8003,
/**
* The cast operation was canceled by the user.
* <br> error.data[0] is an error object from the Cast SDK.
*/
'CAST_CANCELED_BY_USER': 8004,
/**
* The cast connection timed out.
* <br> error.data[0] is an error object from the Cast SDK.
*/
'CAST_CONNECTION_TIMED_OUT': 8005,
/**
* The requested receiver app ID does not exist or is unavailable.
* Check the requested app ID for typos.
* <br> error.data[0] is an error object from the Cast SDK.
*/
'CAST_RECEIVER_APP_UNAVAILABLE': 8006,
/**
* IndexedDb is not supported on this browser; it is required for offline
* support.
*/
'INDEXED_DB_NOT_SUPPORTED': 9000,
/**
* An unknown error occurred in the IndexedDB.
* <br> error.data[0] is the error object.
*/
'INDEXED_DB_ERROR': 9001,
/**
* The operation was aborted. For example, by a call to destroy().
*/
'OPERATION_ABORTED': 9002,
/**
* The specified item was not found in the IndexedDB.
* <br> error.data[0] is the offline URI.
*/
'REQUESTED_ITEM_NOT_FOUND': 9003,
/**
* A network request was made with a malformed offline URI.
* <br> error.data[0] is the URI.
*/
'MALFORMED_OFFLINE_URI': 9004,
/**
* The specified content is live or in-progress.
* Live and in-progress streams cannot be stored offline.
* <br> error.data[0] is the URI.
*/
'CANNOT_STORE_LIVE_OFFLINE': 9005,
/**
* There is already a store operation in-progress, wait until it completes
* before starting another.
*/
'STORE_ALREADY_IN_PROGRESS': 9006,
/**
* The specified manifest is encrypted but does not specify any init data.
* Without init data specified in the manifest, the content will not be
* playable offline.
* <br> error.data[0] is the URI.
*/
'NO_INIT_DATA_FOR_OFFLINE': 9007
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.EventManager');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.MultiMap');
/**
* Creates a new EventManager. An EventManager maintains a collection of "event
* bindings" between event targets and event listeners.
*
* @struct
* @constructor
* @implements {shaka.util.IDestroyable}
*/
shaka.util.EventManager = function() {
/**
* Maps an event type to an array of event bindings.
* @private {shaka.util.MultiMap.<!shaka.util.EventManager.Binding_>}
*/
this.bindingMap_ = new shaka.util.MultiMap();
};
/**
* @typedef {function(!Event)}
*/
shaka.util.EventManager.ListenerType;
/**
* Detaches all event listeners.
* @override
*/
shaka.util.EventManager.prototype.destroy = function() {
this.removeAll();
this.bindingMap_ = null;
return Promise.resolve();
};
/**
* Attaches an event listener to an event target.
* @param {EventTarget} target The event target.
* @param {string} type The event type.
* @param {shaka.util.EventManager.ListenerType} listener The event listener.
*/
shaka.util.EventManager.prototype.listen = function(target, type, listener) {
var binding = new shaka.util.EventManager.Binding_(target, type, listener);
this.bindingMap_.push(type, binding);
};
/**
* Detaches an event listener from an event target.
* @param {EventTarget} target The event target.
* @param {string} type The event type.
*/
shaka.util.EventManager.prototype.unlisten = function(target, type) {
var list = this.bindingMap_.get(type) || [];
for (var i = 0; i < list.length; ++i) {
var binding = list[i];
if (binding.target == target) {
binding.unlisten();
this.bindingMap_.remove(type, binding);
}
}
};
/**
* Detaches all event listeners from all targets.
*/
shaka.util.EventManager.prototype.removeAll = function() {
var list = this.bindingMap_.getAll();
for (var i = 0; i < list.length; ++i) {
list[i].unlisten();
}
this.bindingMap_.clear();
};
/**
* Creates a new Binding_ and attaches the event listener to the event target.
* @param {EventTarget} target The event target.
* @param {string} type The event type.
* @param {shaka.util.EventManager.ListenerType} listener The event listener.
* @constructor
* @private
*/
shaka.util.EventManager.Binding_ = function(target, type, listener) {
/** @type {EventTarget} */
this.target = target;
/** @type {string} */
this.type = type;
/** @type {?shaka.util.EventManager.ListenerType} */
this.listener = listener;
this.target.addEventListener(type, listener, false);
};
/**
* Detaches the event listener from the event target. This does nothing if the
* event listener is already detached.
*/
shaka.util.EventManager.Binding_.prototype.unlisten = function() {
if (!this.target)
return;
this.target.removeEventListener(this.type, this.listener, false);
this.target = null;
this.listener = null;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.FakeEvent');
/**
* Create an Event work-alike object based on the dictionary.
* The event should contain all of the same properties from the dict.
*
* @param {string} type
* @param {Object=} opt_dict
* @constructor
* @extends {Event}
*/
shaka.util.FakeEvent = function(type, opt_dict) {
// Take properties from dict if present.
var dict = opt_dict || {};
for (var key in dict) {
this[key] = dict[key];
}
// These Properties below cannot be set by dict. They are all provided for
// compatibility with native events.
/** @const {boolean} */
this.bubbles = false;
/** @const {boolean} */
this.cancelable = false;
/** @const {boolean} */
this.defaultPrevented = false;
/**
* According to MDN, Chrome uses high-res timers instead of epoch time.
* Follow suit so that timeStamps on FakeEvents use the same base as
* on native Events.
* @const {number}
* @see https://developer.mozilla.org/en-US/docs/Web/API/Event/timeStamp
*/
this.timeStamp = window.performance && window.performance.now ?
window.performance.now() : Date.now();
/** @const {string} */
this.type = type;
/** @const {boolean} */
this.isTrusted = false;
/** @type {EventTarget} */
this.currentTarget = null;
/** @type {EventTarget} */
this.target = null;
/**
* Non-standard property read by FakeEventTarget to stop processing listeners.
* @type {boolean}
*/
this.stopped = false;
};
/**
* Does nothing, since FakeEvents have no default. Provided for compatibility
* with native Events.
* @override
*/
shaka.util.FakeEvent.prototype.preventDefault = function() {};
/**
* Stops processing event listeners for this event. Provided for compatibility
* with native Events.
* @override
*/
shaka.util.FakeEvent.prototype.stopImmediatePropagation = function() {
this.stopped = true;
};
/**
* Does nothing, since FakeEvents do not bubble. Provided for compatibility
* with native Events.
* @override
*/
shaka.util.FakeEvent.prototype.stopPropagation = function() {};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.FakeEventTarget');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.MultiMap');
/**
* A work-alike for EventTarget. Only DOM elements may be true EventTargets,
* but this can be used as a base class to provide event dispatch to non-DOM
* classes. Only FakeEvents should be dispatched.
*
* @struct
* @constructor
* @implements {EventTarget}
* @exportInterface
*/
shaka.util.FakeEventTarget = function() {
/**
* @private {!shaka.util.MultiMap.<shaka.util.FakeEventTarget.ListenerType>}
*/
this.listeners_ = new shaka.util.MultiMap();
/**
* The target of all dispatched events. Defaults to |this|.
* @type {EventTarget}
*/
this.dispatchTarget = this;
};
/**
* These are the listener types defined in the closure extern for EventTarget.
* @typedef {EventListener|function(!Event):(boolean|undefined)}
* @exportInterface
*/
shaka.util.FakeEventTarget.ListenerType;
/**
* Add an event listener to this object.
*
* @param {string} type The event type to listen for.
* @param {shaka.util.FakeEventTarget.ListenerType} listener The callback or
* listener object to invoke.
* @param {boolean=} opt_capturing Ignored. FakeEventTargets do not have
* parents, so events neither capture nor bubble.
* @override
* @exportInterface
*/
shaka.util.FakeEventTarget.prototype.addEventListener =
function(type, listener, opt_capturing) {
this.listeners_.push(type, listener);
};
/**
* Remove an event listener from this object.
*
* @param {string} type The event type for which you wish to remove a listener.
* @param {shaka.util.FakeEventTarget.ListenerType} listener The callback or
* listener object to remove.
* @param {boolean=} opt_capturing Ignored. FakeEventTargets do not have
* parents, so events neither capture nor bubble.
* @override
* @exportInterface
*/
shaka.util.FakeEventTarget.prototype.removeEventListener =
function(type, listener, opt_capturing) {
this.listeners_.remove(type, listener);
};
/**
* Dispatch an event from this object.
*
* @param {!Event} event The event to be dispatched from this object.
* @return {boolean} True if the default action was prevented.
* @override
* @exportInterface
*/
shaka.util.FakeEventTarget.prototype.dispatchEvent = function(event) {
// In many browsers, it is complex to overwrite properties of actual Events.
// Here we expect only to dispatch FakeEvents, which are simpler.
goog.asserts.assert(event instanceof shaka.util.FakeEvent,
'FakeEventTarget can only dispatch FakeEvents!');
var list = this.listeners_.get(event.type) || [];
for (var i = 0; i < list.length; ++i) {
// Do this every time, since events can be re-dispatched from handlers.
event.target = this.dispatchTarget;
event.currentTarget = this.dispatchTarget;
var listener = list[i];
try {
if (listener.handleEvent) {
listener.handleEvent(event);
} else {
listener.call(this, event);
}
} catch (exception) {
// Exceptions during event handlers should not affect the caller,
// but should appear on the console as uncaught, according to MDN:
// http://goo.gl/N6Ff27
shaka.log.error('Uncaught exception in event handler', exception);
}
if (event.stopped) {
break;
}
}
return event.defaultPrevented;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.Functional');
/**
* @namespace shaka.util.Functional
* @summary A set of functional utility functions.
*/
/**
* Creates a promise chain that calls the given callback for each element in
* the array in a catch of a promise.
*
* e.g.:
* Promise.reject().catch(callback(array[0])).catch(callback(array[1]));
*
* @param {!Array.<ELEM>} array
* @param {function(ELEM):!Promise.<RESULT>} callback
* @return {!Promise.<RESULT>}
* @template ELEM,RESULT
*/
shaka.util.Functional.createFallbackPromiseChain = function(array, callback) {
return array.reduce(function(callback, promise, elem) {
return promise.catch(callback.bind(null, elem));
}.bind(null, callback), Promise.reject());
};
/**
* Returns the first array concatenated to the second; used to collapse an
* array of arrays into a single array.
*
* @param {!Array.<T>} all
* @param {!Array.<T>} part
* @return {!Array.<T>}
* @template T
*/
shaka.util.Functional.collapseArrays = function(all, part) {
return all.concat(part);
};
/**
* A no-op function. Useful in promise chains.
*/
shaka.util.Functional.noop = function() {};
/**
* Returns if the given value is not null; useful for filtering out null values.
*
* @param {T} value
* @return {boolean}
* @template T
*/
shaka.util.Functional.isNotNull = function(value) {
return value != null;
};
/**
* Creates a function that returns whether the given value is equal to the given
* value.
*
* @param {T} compare
* @return {function(T):boolean}
* @template T
*/
shaka.util.Functional.isEqualFunc = function(compare) {
return function(a) { return a == compare; };
};
/**
* Creates a function that returns whether the given value is not equal to the
* given value.
*
* @param {T} compare
* @return {function(T):boolean}
* @template T
*/
shaka.util.Functional.isNotEqualFunc = function(compare) {
return function(a) { return a != compare; };
};
/**
* Used to filter out duplicates in an array.
* Returns true the first time the element is encountered. Returns false
* for all the subsequent encounters.
*
* @param {T} item
* @param {number} position
* @param {!Array.<T>} self
* @return {boolean}
* @template T
* @example [1, 1, 2].filter(shaka.util.Functional.isNotDuplicate) -> [1, 2]
*/
shaka.util.Functional.isNotDuplicate = function(item, position, self) {
return self.indexOf(item) == position;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.IDestroyable');
/**
* An interface to standardize how objects are destroyed.
* @interface
* @exportInterface
*/
shaka.util.IDestroyable = function() {};
/**
* Destroys the object, releasing all resources and shutting down all
* operations. Returns a Promise which is resolved when destruction is
* complete. This Promise should never be rejected.
*
* @return {!Promise}
* @exportInterface
*/
shaka.util.IDestroyable.prototype.destroy = function() {};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.LanguageUtils');
goog.require('goog.asserts');
/**
* @namespace shaka.util.LanguageUtils
* @summary A set of language utility functions.
*/
/**
* Compares two language tags as defined by RFC 5646 and ISO 639. The
* comparison takes sublanguages into account via the |fuzz| parameter.
* The caller is expected to normalize the inputs first.
*
* @see shaka.util.LanguageUtils.normalize()
* @see IETF RFC 5646
* @see ISO 639
*
* @param {shaka.util.LanguageUtils.MatchType} fuzz What kind of match is
* acceptable.
* @param {string} preference The user's preferred language tag.
* @param {string} candidate An available language tag.
* @return {boolean}
*/
shaka.util.LanguageUtils.match = function(fuzz, preference, candidate) {
// Alias.
var LanguageUtils = shaka.util.LanguageUtils;
goog.asserts.assert(preference == LanguageUtils.normalize(preference),
'Language pref should be normalized first');
goog.asserts.assert(candidate == LanguageUtils.normalize(candidate),
'Language candidate should be normalized first');
if (candidate == preference) {
return true;
}
if (fuzz >= shaka.util.LanguageUtils.MatchType.BASE_LANGUAGE_OKAY &&
candidate == preference.split('-')[0]) {
return true;
}
if (fuzz >= shaka.util.LanguageUtils.MatchType.OTHER_SUB_LANGUAGE_OKAY &&
candidate.split('-')[0] == preference.split('-')[0]) {
return true;
}
return false;
};
/**
* A match type for fuzzy-matching logic.
*
* @enum {number}
*/
shaka.util.LanguageUtils.MatchType = {
/** Accepts an exact match. */
EXACT: 0,
/** Accepts a less-specific version of the preferred sublanguage. */
BASE_LANGUAGE_OKAY: 1,
/** Accepts a different sublanguage of the preferred base language. */
OTHER_SUB_LANGUAGE_OKAY: 2
};
/**
* Normalize the language tag.
*
* RFC 5646 specifies that language tags are case insensitive and that the
* shortest representation of the base language should always be used.
* This will convert the tag to lower-case and map 3-letter codes (ISO 639-2)
* to 2-letter codes (ISO 639-1) whenever possible.
*
* @param {string} lang
* @return {string}
*
* @see IETF RFC 5646
* @see ISO 639
*/
shaka.util.LanguageUtils.normalize = function(lang) {
var fields = lang.toLowerCase().split('-');
var base = fields[0];
var replacement = shaka.util.LanguageUtils.isoMap_[base];
if (replacement) {
fields[0] = replacement;
}
return fields.join('-');
};
/**
* A map from 3-letter language codes (ISO 639-2) to 2-letter language codes
* (ISO 639-1) for all languages which have both in the registry.
*
* @const {!Object.<string, string>}
* @private
*/
shaka.util.LanguageUtils.isoMap_ = {
'aar': 'aa', 'abk': 'ab', 'afr': 'af', 'aka': 'ak', 'alb': 'sq', 'amh': 'am',
'ara': 'ar', 'arg': 'an', 'arm': 'hy', 'asm': 'as', 'ava': 'av', 'ave': 'ae',
'aym': 'ay', 'aze': 'az', 'bak': 'ba', 'bam': 'bm', 'baq': 'eu', 'bel': 'be',
'ben': 'bn', 'bih': 'bh', 'bis': 'bi', 'bod': 'bo', 'bos': 'bs', 'bre': 'br',
'bul': 'bg', 'bur': 'my', 'cat': 'ca', 'ces': 'cs', 'cha': 'ch', 'che': 'ce',
'chi': 'zh', 'chu': 'cu', 'chv': 'cv', 'cor': 'kw', 'cos': 'co', 'cre': 'cr',
'cym': 'cy', 'cze': 'cs', 'dan': 'da', 'deu': 'de', 'div': 'dv', 'dut': 'nl',
'dzo': 'dz', 'ell': 'el', 'eng': 'en', 'epo': 'eo', 'est': 'et', 'eus': 'eu',
'ewe': 'ee', 'fao': 'fo', 'fas': 'fa', 'fij': 'fj', 'fin': 'fi', 'fra': 'fr',
'fre': 'fr', 'fry': 'fy', 'ful': 'ff', 'geo': 'ka', 'ger': 'de', 'gla': 'gd',
'gle': 'ga', 'glg': 'gl', 'glv': 'gv', 'gre': 'el', 'grn': 'gn', 'guj': 'gu',
'hat': 'ht', 'hau': 'ha', 'heb': 'he', 'her': 'hz', 'hin': 'hi', 'hmo': 'ho',
'hrv': 'hr', 'hun': 'hu', 'hye': 'hy', 'ibo': 'ig', 'ice': 'is', 'ido': 'io',
'iii': 'ii', 'iku': 'iu', 'ile': 'ie', 'ina': 'ia', 'ind': 'id', 'ipk': 'ik',
'isl': 'is', 'ita': 'it', 'jav': 'jv', 'jpn': 'ja', 'kal': 'kl', 'kan': 'kn',
'kas': 'ks', 'kat': 'ka', 'kau': 'kr', 'kaz': 'kk', 'khm': 'km', 'kik': 'ki',
'kin': 'rw', 'kir': 'ky', 'kom': 'kv', 'kon': 'kg', 'kor': 'ko', 'kua': 'kj',
'kur': 'ku', 'lao': 'lo', 'lat': 'la', 'lav': 'lv', 'lim': 'li', 'lin': 'ln',
'lit': 'lt', 'ltz': 'lb', 'lub': 'lu', 'lug': 'lg', 'mac': 'mk', 'mah': 'mh',
'mal': 'ml', 'mao': 'mi', 'mar': 'mr', 'may': 'ms', 'mkd': 'mk', 'mlg': 'mg',
'mlt': 'mt', 'mon': 'mn', 'mri': 'mi', 'msa': 'ms', 'mya': 'my', 'nau': 'na',
'nav': 'nv', 'nbl': 'nr', 'nde': 'nd', 'ndo': 'ng', 'nep': 'ne', 'nld': 'nl',
'nno': 'nn', 'nob': 'nb', 'nor': 'no', 'nya': 'ny', 'oci': 'oc', 'oji': 'oj',
'ori': 'or', 'orm': 'om', 'oss': 'os', 'pan': 'pa', 'per': 'fa', 'pli': 'pi',
'pol': 'pl', 'por': 'pt', 'pus': 'ps', 'que': 'qu', 'roh': 'rm', 'ron': 'ro',
'rum': 'ro', 'run': 'rn', 'rus': 'ru', 'sag': 'sg', 'san': 'sa', 'sin': 'si',
'slk': 'sk', 'slo': 'sk', 'slv': 'sl', 'sme': 'se', 'smo': 'sm', 'sna': 'sn',
'snd': 'sd', 'som': 'so', 'sot': 'st', 'spa': 'es', 'sqi': 'sq', 'srd': 'sc',
'srp': 'sr', 'ssw': 'ss', 'sun': 'su', 'swa': 'sw', 'swe': 'sv', 'tah': 'ty',
'tam': 'ta', 'tat': 'tt', 'tel': 'te', 'tgk': 'tg', 'tgl': 'tl', 'tha': 'th',
'tib': 'bo', 'tir': 'ti', 'ton': 'to', 'tsn': 'tn', 'tso': 'ts', 'tuk': 'tk',
'tur': 'tr', 'twi': 'tw', 'uig': 'ug', 'ukr': 'uk', 'urd': 'ur', 'uzb': 'uz',
'ven': 've', 'vie': 'vi', 'vol': 'vo', 'wel': 'cy', 'wln': 'wa', 'wol': 'wo',
'xho': 'xh', 'yid': 'yi', 'yor': 'yo', 'zha': 'za', 'zho': 'zh', 'zul': 'zu'
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.MapUtils');
/**
* @namespace shaka.util.MapUtils
* @summary A set of map/object utility functions.
*/
/**
* Returns true if the map is empty; otherwise, returns false.
*
* @param {Object.<KEY, VALUE>} object
* @return {boolean}
* @template KEY,VALUE
*/
shaka.util.MapUtils.empty = function(object) {
return !object || Object.keys(object).length == 0;
};
/**
* Gets the map's values.
*
* @param {!Object.<KEY, VALUE>} object
* @return {!Array.<VALUE>}
* @template KEY,VALUE
*/
shaka.util.MapUtils.values = function(object) {
return Object.keys(object).map(function(key) { return object[key]; });
};
/**
* Converts the values in the given Map to a different value.
*
* @param {!Object.<KEY, VALUE>} object
* @param {function(VALUE, KEY=):OUTPUT} callback
* @return {!Object.<KEY, OUTPUT>}
* @template KEY,VALUE,OUTPUT
*/
shaka.util.MapUtils.map = function(object, callback) {
return Object.keys(object).reduce(function(ret, key) {
var value = object[key];
ret[key] = callback(value, key);
return ret;
}, {});
};
/**
* Creates a new object where the values are filtered out according to a
* predicate.
*
* @param {!Object.<KEY, VALUE>} object
* @param {function(KEY, VALUE):boolean} callback
* @return {!Object.<KEY, VALUE>}
* @template KEY,VALUE
*/
shaka.util.MapUtils.filter = function(object, callback) {
return Object.keys(object).reduce(function(ret, key) {
if (callback(key, object[key]))
ret[key] = object[key];
return ret;
}, {});
};
/**
* Returns true if every entry matches the predicate.
*
* @param {!Object.<KEY, VALUE>} object
* @param {function(KEY, VALUE):boolean} callback
* @return {boolean}
* @template KEY,VALUE
*/
shaka.util.MapUtils.every = function(object, callback) {
return Object.keys(object).every(function(key) {
return callback(key, object[key]);
});
};
/**
* Returns true if any entry matches the predicate.
*
* @param {!Object.<KEY, VALUE>} object
* @param {function(KEY, VALUE):boolean} callback
* @return {boolean}
* @template KEY,VALUE
*/
shaka.util.MapUtils.some = function(object, callback) {
return Object.keys(object).some(function(key) {
return callback(key, object[key]);
});
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.Mp4Parser');
goog.require('shaka.util.DataViewReader');
/**
* Looks for a box of a specified type in an mp4 stream and returns
* it's size if found one. Returns -1 otherwise. Advances the
* position of the reader to the beginning of the box content.
* @param {number} boxType
* @param {!shaka.util.DataViewReader} reader
* @return {number}
*/
shaka.util.Mp4Parser.findBox = function(boxType, reader) {
while (reader.hasMoreData()) {
var start = reader.getPosition();
var size = reader.readUint32();
var type = reader.readUint32();
if (size == 1) {
size = reader.readUint64();
} else if (size == 0) {
size = reader.getLength() - start;
}
if (type == boxType) {
return size;
} else {
reader.skip(size - (reader.getPosition() - start));
continue;
}
}
// couldn't find the box
return shaka.util.Mp4Parser.BOX_NOT_FOUND;
};
/**
* Finds a specified child of the sample description box
* traversing a given path and returns it's size. Returns -1
* if the box was not found.
* @param {ArrayBuffer} data
* @param {number} boxType
* @return {number}
*/
shaka.util.Mp4Parser.findSampleDescriptionBox = function(data, boxType) {
var reader = new shaka.util.DataViewReader(
new DataView(data),
shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
/* Path to a sample description box:
moov->trak->mdia->minf->stpl->stsd->
*/
var Mp4Parser = shaka.util.Mp4Parser;
var path = [
//[box type, number of bytes to skip before getting to inner boxes
// (for flags etc.)]
[Mp4Parser.BOX_TYPE_MOOV, 0],
[Mp4Parser.BOX_TYPE_TRAK, 0],
[Mp4Parser.BOX_TYPE_MDIA, 0],
[Mp4Parser.BOX_TYPE_MINF, 0],
[Mp4Parser.BOX_TYPE_STBL, 0],
[Mp4Parser.BOX_TYPE_STSD, 8],
[boxType, 0]
];
var size = Mp4Parser.BOX_NOT_FOUND;
for (var i = 0; i < path.length; i++) {
var type = path[i][0];
var skipBytes = path[i][1];
size = Mp4Parser.findBox(type, reader);
if (size == Mp4Parser.BOX_NOT_FOUND)
return Mp4Parser.BOX_NOT_FOUND;
reader.skip(skipBytes);
}
return size;
};
/** @const {number} */
shaka.util.Mp4Parser.BOX_NOT_FOUND = -1;
/** @const {number} */
shaka.util.Mp4Parser.BOX_TYPE_MDAT = 0x6D646174;
/** @const {number} */
shaka.util.Mp4Parser.BOX_TYPE_MOOV = 0x6D6F6F76;
/** @const {number} */
shaka.util.Mp4Parser.BOX_TYPE_TRAK = 0x7472616B;
/** @const {number} */
shaka.util.Mp4Parser.BOX_TYPE_MDIA = 0x6D646961;
/** @const {number} */
shaka.util.Mp4Parser.BOX_TYPE_MINF = 0x6D696E66;
/** @const {number} */
shaka.util.Mp4Parser.BOX_TYPE_STBL = 0x7374626C;
/** @const {number} */
shaka.util.Mp4Parser.BOX_TYPE_STSD = 0x73747364;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.MultiMap');
/**
* A simple multimap template.
* @constructor
* @struct
* @template T
*/
shaka.util.MultiMap = function() {
/** @private {!Object.<string, !Array.<T>>} */
this.map_ = {};
};
/**
* Add a key, value pair to the map.
* @param {string} key
* @param {T} value
*/
shaka.util.MultiMap.prototype.push = function(key, value) {
if (this.map_.hasOwnProperty(key)) {
this.map_[key].push(value);
} else {
this.map_[key] = [value];
}
};
/**
* Set an array of values for the key, overwriting any previous data.
* @param {string} key
* @param {!Array.<T>} values
*/
shaka.util.MultiMap.prototype.set = function(key, values) {
this.map_[key] = values;
};
/**
* Check for a key.
* @param {string} key
* @return {boolean} true if the key exists.
*/
shaka.util.MultiMap.prototype.has = function(key) {
return this.map_.hasOwnProperty(key);
};
/**
* Get a list of values by key.
* @param {string} key
* @return {Array.<T>} or null if no such key exists.
*/
shaka.util.MultiMap.prototype.get = function(key) {
var list = this.map_[key];
// slice() clones the list so that it and the map can each be modified
// without affecting the other.
return list ? list.slice() : null;
};
/**
* Get a list of all values.
* @return {!Array.<T>}
*/
shaka.util.MultiMap.prototype.getAll = function() {
var list = [];
for (var key in this.map_) {
list.push.apply(list, this.map_[key]);
}
return list;
};
/**
* Remove a specific value, if it exists.
* @param {string} key
* @param {T} value
*/
shaka.util.MultiMap.prototype.remove = function(key, value) {
var list = this.map_[key];
if (!list) return;
for (var i = 0; i < list.length; ++i) {
if (list[i] == value) {
list.splice(i, 1);
--i;
}
}
};
/**
* Get all keys from the multimap.
* @return {!Array.<string>}
*/
shaka.util.MultiMap.prototype.keys = function() {
var result = [];
for (var key in this.map_) {
result.push(key);
}
return result;
};
/**
* Clear all keys and values from the multimap.
*/
shaka.util.MultiMap.prototype.clear = function() {
this.map_ = {};
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.Pssh');
goog.require('shaka.log');
goog.require('shaka.util.DataViewReader');
goog.require('shaka.util.Mp4Parser');
goog.require('shaka.util.Uint8ArrayUtils');
/**
* Parse a PSSH box and extract the system IDs.
*
* @param {!Uint8Array} psshBox
* @constructor
* @struct
* @throws {shaka.util.Error} if a PSSH box is truncated or contains a size
* field over 53 bits.
*/
shaka.util.Pssh = function(psshBox) {
/**
* In hex.
* @type {!Array.<string>}
*/
this.systemIds = [];
/**
* In hex.
* @type {!Array.<string>}
*/
this.cencKeyIds = [];
/*
* Array of tuples that define the startIndex + size for each
* discrete pssh within |psshBox|
* */
this.dataBoundaries = [];
// Parse the PSSH box.
var reader = new shaka.util.DataViewReader(
new DataView(psshBox.buffer),
shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
var psshBoxFound = false;
// There could be multiple boxes concatenated together.
while (reader.hasMoreData()) {
var size = shaka.util.Mp4Parser.findBox(shaka.util.Pssh.BOX_TYPE, reader);
if (size == shaka.util.Mp4Parser.BOX_NOT_FOUND)
break;
// a file can have several pssh boxes, mark that at least
// one was found
psshBoxFound = true;
var startPosition = reader.getPosition() - 8;
var version = reader.readUint8();
if (version > 1) {
shaka.log.warning('Unrecognized PSSH version found!');
reader.skip(size - (reader.getPosition() - startPosition));
continue;
}
reader.skip(3); // Skip flags.
var systemId = shaka.util.Uint8ArrayUtils.toHex(reader.readBytes(16));
var keyIds = [];
if (version > 0) {
var numKeyIds = reader.readUint32();
for (var i = 0; i < numKeyIds; ++i) {
var keyId = shaka.util.Uint8ArrayUtils.toHex(reader.readBytes(16));
keyIds.push(keyId);
}
}
var dataSize = reader.readUint32();
reader.skip(dataSize); // Ignore the data section.
// Now that everything has been succesfully parsed from this box,
// update member variables.
this.cencKeyIds.push.apply(this.cencKeyIds, keyIds);
this.systemIds.push(systemId);
this.dataBoundaries.push({
start: startPosition,
end: reader.getPosition() - 1 }
);
if (reader.getPosition() != startPosition + size) {
shaka.log.warning('Mismatch between box size and data size!');
reader.skip(size - (reader.getPosition() - startPosition));
}
}
if (!psshBoxFound)
shaka.log.warning('No pssh box found!');
};
/** @const {number} */
shaka.util.Pssh.BOX_TYPE = 0x70737368;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.PublicPromise');
/**
* A utility to create Promises with convenient public resolve and reject
* methods.
*
* @constructor
* @struct
* @extends {Promise.<T>}
* @return {Promise.<T>}
* @template T
*/
shaka.util.PublicPromise = function() {
var resolvePromise;
var rejectPromise;
// Promise.call causes an error. It seems that inheriting from a native
// Promise is not permitted by JavaScript interpreters.
// The work-around is to construct a Promise object, modify it to look like
// the compiler's picture of PublicPromise, then return it. The caller of
// new PublicPromise will receive |promise| instead of |this|, and the
// compiler will be aware of the additional properties |resolve| and
// |reject|.
var promise = new Promise(function(resolve, reject) {
resolvePromise = resolve;
rejectPromise = reject;
});
promise.resolve = resolvePromise;
promise.reject = rejectPromise;
return promise;
};
/** @type {function(T=)} */
shaka.util.PublicPromise.prototype.resolve;
/** @type {function(*=)} */
shaka.util.PublicPromise.prototype.reject;
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.StreamUtils');
goog.require('shaka.log');
goog.require('shaka.media.DrmEngine');
goog.require('shaka.media.MediaSourceEngine');
goog.require('shaka.util.Functional');
goog.require('shaka.util.LanguageUtils');
/**
* @param {shakaExtern.Period} period
* @param {shakaExtern.Restrictions} restrictions
* Configured restrictions from the user.
* @param {{width: number, height: number}} maxHwRes
* The maximum resolution the hardware can handle.
* This is applied separately from user restrictions because the setting
* should not be easily replaced by the user's configuration.
* @return {boolean} Whether the tracks changed.
*/
shaka.util.StreamUtils.applyRestrictions =
function(period, restrictions, maxHwRes) {
var tracksChanged = false;
period.streamSets.forEach(function(streamSet) {
streamSet.streams.forEach(function(stream) {
var originalAllowed = stream.allowedByApplication;
stream.allowedByApplication = true;
if (streamSet.type == 'video') {
if (stream.width < restrictions.minWidth ||
stream.width > restrictions.maxWidth ||
stream.width > maxHwRes.width ||
stream.height < restrictions.minHeight ||
stream.height > restrictions.maxHeight ||
stream.height > maxHwRes.height ||
(stream.width * stream.height) < restrictions.minPixels ||
(stream.width * stream.height) > restrictions.maxPixels ||
stream.bandwidth < restrictions.minVideoBandwidth ||
stream.bandwidth > restrictions.maxVideoBandwidth) {
stream.allowedByApplication = false;
}
} else if (streamSet.type == 'audio') {
if (stream.bandwidth < restrictions.minAudioBandwidth ||
stream.bandwidth > restrictions.maxAudioBandwidth) {
stream.allowedByApplication = false;
}
}
if (originalAllowed != stream.allowedByApplication)
tracksChanged = true;
});
});
return tracksChanged;
};
/**
* Alters the given Period to filter out any unplayable streams.
*
* @param {shaka.media.DrmEngine} drmEngine
* @param {!Object.<string, shakaExtern.Stream>} activeStreams
* @param {shakaExtern.Period} period
*/
shaka.util.StreamUtils.filterPeriod = function(
drmEngine, activeStreams, period) {
var keySystem = '';
var drmSupportedMimeTypes = null;
if (drmEngine && drmEngine.initialized()) {
keySystem = drmEngine.keySystem();
drmSupportedMimeTypes = drmEngine.getSupportedTypes();
}
for (var i = 0; i < period.streamSets.length; ++i) {
var streamSet = period.streamSets[i];
if (keySystem) {
// A key system has been selected.
// Remove streamSets which can only be used with other key systems.
// Note that drmInfos == [] means unencrypted.
var match = streamSet.drmInfos.length == 0 ||
streamSet.drmInfos.some(function(drmInfo) {
return drmInfo.keySystem == keySystem; });
if (!match) {
shaka.log.debug('Dropping StreamSet, can\'t be used with ' + keySystem,
streamSet);
period.streamSets.splice(i, 1);
--i;
continue;
}
}
var activeStream = activeStreams[streamSet.type];
for (var j = 0; j < streamSet.streams.length; ++j) {
var stream = streamSet.streams[j];
var fullMimeType = shaka.util.StreamUtils.getFullMimeType(
stream.mimeType, stream.codecs);
if (!shaka.media.MediaSourceEngine.isTypeSupported(fullMimeType)) {
// Remove streams that cannot be played by the platform.
streamSet.streams.splice(j, 1);
--j;
continue;
}
if (drmSupportedMimeTypes && stream.encrypted &&
drmSupportedMimeTypes.indexOf(fullMimeType) < 0) {
// Remove encrypted streams that cannot be handled by the key system.
streamSet.streams.splice(j, 1);
--j;
continue;
}
if (activeStream) {
// Check that the basic mime types and basic codecs match.
// For example, we can't adapt between WebM and MP4,
// nor can we adapt between mp4a.* to ec-3.
if (stream.mimeType != activeStream.mimeType ||
stream.codecs.split('.')[0] != activeStream.codecs.split('.')[0]) {
streamSet.streams.splice(j, 1);
--j;
continue;
}
}
}
if (streamSet.streams.length == 0) {
period.streamSets.splice(i, 1);
--i;
}
}
};
/**
* Gets an array of Track objects for the given Period
*
* @param {shakaExtern.Period} period
* @param {Object.<string, shakaExtern.Stream>} activeStreams
* @return {!Array.<shakaExtern.Track>}
*/
shaka.util.StreamUtils.getTracks = function(period, activeStreams) {
// Convert each stream into a track and squash them into one array.
var Functional = shaka.util.Functional;
return period.streamSets
.map(function(streamSet) {
var activeStream = activeStreams ? activeStreams[streamSet.type] : null;
return streamSet.streams
.filter(function(stream) {
return stream.allowedByApplication && stream.allowedByKeySystem;
})
.map(function(stream) {
return {
id: stream.id,
active: activeStream == stream,
type: streamSet.type,
bandwidth: stream.bandwidth,
language: streamSet.language,
kind: stream.kind || null,
width: stream.width || null,
height: stream.height || null,
frameRate: stream.frameRate || undefined,
codecs: stream.codecs || null
};
});
})
.reduce(Functional.collapseArrays, []);
};
/**
* Find the stream and stream set for the given track.
*
* @param {shakaExtern.Period} period
* @param {shakaExtern.Track} track
* @return {?{stream: shakaExtern.Stream, streamSet: shakaExtern.StreamSet}}
*/
shaka.util.StreamUtils.findStreamForTrack = function(period, track) {
for (var i = 0; i < period.streamSets.length; i++) {
var streamSet = period.streamSets[i];
for (var j = 0; j < streamSet.streams.length; j++) {
var stream = streamSet.streams[j];
if (stream.id == track.id)
return {stream: stream, streamSet: streamSet};
}
}
return null;
};
/**
* Determines if the given stream set has any playable streams.
* @param {shakaExtern.StreamSet} streamSet
* @return {boolean}
*/
shaka.util.StreamUtils.hasPlayableStreams = function(streamSet) {
return streamSet.streams.some(function(stream) {
return stream.allowedByApplication && stream.allowedByKeySystem;
});
};
/**
* Chooses a stream set of each type according to the given config.
*
* @param {shakaExtern.Period} period
* @param {shakaExtern.PlayerConfiguration} config
* @param {!Object=} opt_languageMatches
* @return {!Object.<string, shakaExtern.StreamSet>}
*/
shaka.util.StreamUtils.chooseStreamSets = function(
period, config, opt_languageMatches) {
var LanguageUtils = shaka.util.LanguageUtils;
var hasPlayableStreams = shaka.util.StreamUtils.hasPlayableStreams;
var StreamUtils = shaka.util.StreamUtils;
// Choose the first stream set listed as the default.
/** @type {!Object.<string, shakaExtern.StreamSet>} */
var streamSetsByType = {};
period.streamSets.forEach(function(set) {
if (!hasPlayableStreams(set) || set.type in streamSetsByType) return;
streamSetsByType[set.type] = set;
});
// Pick video set with highest top resolution, break ties
// by selecting one with lower average bandwidth
var highestResolution = 0;
period.streamSets.forEach(function(set) {
if (hasPlayableStreams(set) && set.type == 'video') {
var resolution = StreamUtils.getHighestResolution(set);
if (resolution > highestResolution) {
highestResolution = resolution;
streamSetsByType['video'] = set;
} else if (resolution == highestResolution) {
if (StreamUtils.getAvgBandwidth(set) <
StreamUtils.getAvgBandwidth(streamSetsByType['video'])) {
streamSetsByType['video'] = set;
}
}
}
});
// Then if there are primary stream sets, override the default.
period.streamSets.forEach(function(set) {
if (hasPlayableStreams(set) && set.primary) {
// if both sets are primary, choose one with lower
// average bandwidth
if (streamSetsByType[set.type].primary) {
if (StreamUtils.getAvgBandwidth(set) <
StreamUtils.getAvgBandwidth(streamSetsByType[set.type])) {
streamSetsByType[set.type] = set;
}
} else {
streamSetsByType[set.type] = set;
}
}
});
// Finally, choose based on language preference. Favor exact matches, then
// base matches, finally different subtags. Execute in reverse order so
// the later steps override the previous ones.
[LanguageUtils.MatchType.OTHER_SUB_LANGUAGE_OKAY,
LanguageUtils.MatchType.BASE_LANGUAGE_OKAY,
LanguageUtils.MatchType.EXACT]
.forEach(function(matchType) {
period.streamSets.forEach(function(set) {
if (!hasPlayableStreams(set))
return;
/** @type {string} */
var pref;
if (set.type == 'audio')
pref = config.preferredAudioLanguage;
else if (set.type == 'text')
pref = config.preferredTextLanguage;
if (pref) {
pref = LanguageUtils.normalize(pref);
var lang = LanguageUtils.normalize(set.language);
if (LanguageUtils.match(matchType, pref, lang)) {
// If this audio stream has the same language as a previous
// match, only choose it if it uses less bandwidth.
if (set.language == streamSetsByType[set.type].language) {
if (StreamUtils.getAvgBandwidth(set) <
StreamUtils.getAvgBandwidth(streamSetsByType[set.type])) {
streamSetsByType[set.type] = set;
}
} else {
streamSetsByType[set.type] = set;
}
if (opt_languageMatches)
opt_languageMatches[set.type] = true;
}
}
});
});
return streamSetsByType;
};
/**
* Computes average bandwidth across all streams of a stream set.
* Assumes a stream set of audio/video type, where all streams have
* bandwidth.
*
* @param {shakaExtern.StreamSet} streamSet
* @return {number}
*/
shaka.util.StreamUtils.getAvgBandwidth = function(streamSet) {
var bandwidthSum = 0;
// to make sure we don't end up trying to divide by 0
if (!streamSet || streamSet.streams.length < 1) return bandwidthSum;
streamSet.streams.forEach(function(stream) {
bandwidthSum += stream.bandwidth;
});
return bandwidthSum / streamSet.streams.length;
};
/**
* Loops through all the streams in a StreamSet and returns the value
* of the highest resolution found across all streams.
* Assumes a valid video StreamSet.
*
* @param {shakaExtern.StreamSet} streamSet
* @return {number}
*/
shaka.util.StreamUtils.getHighestResolution = function(streamSet) {
var highestRes = 0;
if (!streamSet) return highestRes;
streamSet.streams.forEach(function(stream) {
if (stream.height > highestRes)
highestRes = stream.height;
});
return highestRes;
};
/**
* Takes a MIME type and optional codecs string and produces the full MIME type.
*
* @param {string} mimeType
* @param {string=} opt_codecs
* @return {string}
*/
shaka.util.StreamUtils.getFullMimeType = function(mimeType, opt_codecs) {
var fullMimeType = mimeType;
if (opt_codecs) {
fullMimeType += '; codecs="' + opt_codecs + '"';
}
return fullMimeType;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.StringUtils');
goog.require('shaka.log');
goog.require('shaka.util.Error');
/**
* @namespace shaka.util.StringUtils
* @summary A set of string utility functions.
*/
/**
* Creates a string from the given buffer as UTF-8 encoding.
*
* @param {?BufferSource} data
* @return {string}
* @throws {shaka.util.Error}
*/
shaka.util.StringUtils.fromUTF8 = function(data) {
if (!data) return '';
var uint8 = new Uint8Array(data);
// If present, strip off the UTF-8 BOM.
if (uint8[0] == 0xef && uint8[1] == 0xbb && uint8[2] == 0xbf) {
uint8 = uint8.subarray(3);
}
// http://stackoverflow.com/a/13691499
var utf8 = shaka.util.StringUtils.fromCharCode_(uint8);
// This converts each character in the string to an escape sequence. If the
// character is in the ASCII range, it is not converted; otherwise it is
// converted to a URI escape sequence.
// Example: '\x67\x35\xe3\x82\xac' -> 'g#%E3%82%AC'
var escaped = escape(utf8);
// Decode the escaped sequence. This will interpret UTF-8 sequences into the
// correct character.
// Example: 'g#%E3%82%AC' -> 'g#€'
try {
return decodeURIComponent(escaped);
} catch (e) {
throw new shaka.util.Error(
shaka.util.Error.Category.TEXT, shaka.util.Error.Code.BAD_ENCODING);
}
};
/**
* Creates a string from the given buffer as UTF-16 encoding.
*
* @param {?BufferSource} data
* @param {boolean} littleEndian true to read little endian, false to read big.
* @return {string}
* @throws {shaka.util.Error}
*/
shaka.util.StringUtils.fromUTF16 = function(data, littleEndian) {
if (!data) return '';
if (data.byteLength % 2 != 0) {
shaka.log.error('Data has an incorrect length, must be even.');
throw new shaka.util.Error(
shaka.util.Error.Category.TEXT, shaka.util.Error.Code.BAD_ENCODING);
}
/** @type {ArrayBuffer} */
var buffer;
if (data instanceof ArrayBuffer) {
buffer = data;
} else {
// Have to create a new buffer because the argument may be a smaller
// view on a larger ArrayBuffer. We cannot use an ArrayBufferView in
// a DataView.
var temp = new Uint8Array(data.byteLength);
temp.set(new Uint8Array(data));
buffer = temp.buffer;
}
// Use a DataView to ensure correct endianness.
var length = data.byteLength / 2;
var arr = new Uint16Array(length);
var dataView = new DataView(buffer);
for (var i = 0; i < length; i++) {
arr[i] = dataView.getUint16(i * 2, littleEndian);
}
return shaka.util.StringUtils.fromCharCode_(arr);
};
/**
* Creates a string from the given buffer, auto-detecting the encoding that is
* being used. If it cannot detect the encoding, it will throw an exception.
*
* @param {?BufferSource} data
* @return {string}
* @throws {shaka.util.Error}
*/
shaka.util.StringUtils.fromBytesAutoDetect = function(data) {
var StringUtils = shaka.util.StringUtils;
var uint8 = new Uint8Array(data);
if (uint8[0] == 0xef && uint8[1] == 0xbb && uint8[2] == 0xbf)
return StringUtils.fromUTF8(uint8);
else if (uint8[0] == 0xfe && uint8[1] == 0xff)
return StringUtils.fromUTF16(uint8.subarray(2), false /* littleEndian */);
else if (uint8[0] == 0xff && uint8[1] == 0xfe)
return StringUtils.fromUTF16(uint8.subarray(2), true /* littleEndian */);
var isAscii = (function(arr, i) {
// arr[i] >= ' ' && arr[i] <= '~';
return arr.byteLength <= i || (arr[i] >= 0x20 && arr[i] <= 0x7e);
}.bind(null, uint8));
shaka.log.debug('Unable to find byte-order-mark, making an educated guess.');
if (uint8[0] == 0 && uint8[2] == 0)
return StringUtils.fromUTF16(data, false /* littleEndian */);
else if (uint8[1] == 0 && uint8[3] == 0)
return StringUtils.fromUTF16(data, true /* littleEndian */);
else if (isAscii(0) && isAscii(1) && isAscii(2) && isAscii(3))
return StringUtils.fromUTF8(data);
throw new shaka.util.Error(
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.UNABLE_TO_DETECT_ENCODING);
};
/**
* Creates a ArrayBuffer from the given string, converting to UTF-8 encoding.
*
* @param {string} str
* @return {!ArrayBuffer}
*/
shaka.util.StringUtils.toUTF8 = function(str) {
// http://stackoverflow.com/a/13691499
// Converts the given string to a URI encoded string. If a character falls
// in the ASCII range, it is not converted; otherwise it will be converted to
// a series of URI escape sequences according to UTF-8.
// Example: 'g#€' -> 'g#%E3%82%AC'
var encoded = encodeURIComponent(str);
// Convert each escape sequence individually into a character. Each escape
// sequence is interpreted as a code-point, so if an escape sequence happens
// to be part of a multi-byte sequence, each byte will be converted to a
// single character.
// Example: 'g#%E3%82%AC' -> '\x67\x35\xe3\x82\xac'
var utf8 = unescape(encoded);
var result = new Uint8Array(utf8.length);
for (var i = 0; i < utf8.length; ++i) {
result[i] = utf8.charCodeAt(i);
}
return result.buffer;
};
/**
* Creates a new string from the given array of char codes.
*
* @param {!TypedArray} args
* @return {string}
* @private
*/
shaka.util.StringUtils.fromCharCode_ = function(args) {
var max = 16000;
var ret = '';
for (var i = 0; i < args.length; i += max) {
var subArray = args.subarray(i, i + max);
ret += String.fromCharCode.apply(null, subArray);
}
return ret;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.TextParser');
goog.require('goog.asserts');
/**
* Reads elements from strings.
*
* @param {string} data
* @constructor
* @struct
*/
shaka.util.TextParser = function(data) {
/**
* @const
* @private {string}
*/
this.data_ = data;
/** @private {number} */
this.position_ = 0;
};
/** @return {boolean} Whether it is at the end of the string. */
shaka.util.TextParser.prototype.atEnd = function() {
return this.position_ == this.data_.length;
};
/**
* Reads a line from the parser. This will read but not return the newline.
* Returns null at the end.
*
* @return {?string}
*/
shaka.util.TextParser.prototype.readLine = function() {
return this.readRegexReturnCapture_(/(.*?)(\n|$)/gm, 1);
};
/**
* Reads a word from the parser. This will not read or return any whitespace
* before or after the word (including newlines). Returns null at the end.
*
* @return {?string}
*/
shaka.util.TextParser.prototype.readWord = function() {
return this.readRegexReturnCapture_(/[^ \t\n]*/gm, 0);
};
/**
* Skips any continuous whitespace from the parser. Returns null at the end.
*/
shaka.util.TextParser.prototype.skipWhitespace = function() {
this.readRegex(/[ \t]+/gm);
};
/**
* Reads the given regular expression from the parser. This requires the match
* to be at the current position; there is no need to include a head anchor.
* This requires that the regex have the global flag to be set so that it can
* set lastIndex to start the search at the current position. Returns null at
* the end or if the regex does not match the current position.
*
* @param {!RegExp} regex
* @return {Array.<string>}
*/
shaka.util.TextParser.prototype.readRegex = function(regex) {
var index = this.indexOf_(regex);
if (this.atEnd() || index == null || index.position != this.position_)
return null;
this.position_ += index.length;
return index.results;
};
/**
* Reads a regex from the parser and returns the given capture.
*
* @param {!RegExp} regex
* @param {number} index
* @return {?string}
* @private
*/
shaka.util.TextParser.prototype.readRegexReturnCapture_ =
function(regex, index) {
if (this.atEnd())
return null;
var ret = this.readRegex(regex);
if (!ret)
return null;
else
return ret[index];
};
/**
* Returns the index info about a regular expression match.
*
* @param {!RegExp} regex
* @return {?{position: number, length: number, results: !Array.<string>}}
* @private
*/
shaka.util.TextParser.prototype.indexOf_ = function(regex) {
// The global flag is required to use lastIndex.
goog.asserts.assert(regex.global, 'global flag should be set');
regex.lastIndex = this.position_;
var results = regex.exec(this.data_);
if (results == null)
return null;
else
return {
position: results.index,
length: results[0].length,
results: results
};
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.Timer');
/**
* A simple cancelable timer.
* @param {Function} callback
* @constructor
* @struct
*/
shaka.util.Timer = function(callback) {
/** @private {?number} */
this.id_ = null;
/** @private {number} */
this.timeoutSeconds_ = 0;
/** @private {Function} */
this.callback_ = (function() {
this.id_ = null;
callback();
}.bind(this));
};
/**
* Cancel the timer, if it's running.
*/
shaka.util.Timer.prototype.cancel = function() {
if (this.id_ != null) {
clearTimeout(this.id_);
this.id_ = null;
}
};
/**
* Schedule the timer, canceling any previous scheduling.
* @param {number} seconds
*/
shaka.util.Timer.prototype.schedule = function(seconds) {
this.cancel();
this.timeoutSeconds_ = seconds;
this.id_ = setTimeout(this.callback_, seconds * 1000);
};
/**
* If the timer is running, reschedule it using the previous scheduled timeout.
* @example
* If scheduled for 5 seconds, and rescheduled 3 seconds later,
* the timer will fire 8 seconds after the original scheduling.
* @example
* If scheduled for 5 seconds, and rescheduled 6 seconds later,
* the timer will already have fired and will not be rescheduled.
*/
shaka.util.Timer.prototype.rescheduleIfRunning = function() {
if (this.id_ != null) {
this.schedule(this.timeoutSeconds_);
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.Uint8ArrayUtils');
/**
* @namespace shaka.util.Uint8ArrayUtils
* @summary A set of Uint8Array utility functions.
*/
/**
* Convert a Uint8Array to a base64 string. The output will always use the
* alternate encoding/alphabet also known as "base64url".
* @param {!Uint8Array} arr
* @param {boolean=} opt_padding If true, pad the output with equals signs.
* Defaults to true.
* @return {string}
*/
shaka.util.Uint8ArrayUtils.toBase64 = function(arr, opt_padding) {
// btoa expects a "raw string" where each character is interpreted as a byte.
var bytes = String.fromCharCode.apply(null, arr);
var padding = (opt_padding == undefined) ? true : opt_padding;
var base64 = window.btoa(bytes).replace(/\+/g, '-').replace(/\//g, '_');
return padding ? base64 : base64.replace(/=*$/, '');
};
/**
* Convert a base64 string to a Uint8Array. Accepts either the standard
* alphabet or the alternate "base64url" alphabet.
* @param {string} str
* @return {!Uint8Array}
*/
shaka.util.Uint8ArrayUtils.fromBase64 = function(str) {
// atob creates a "raw string" where each character is interpreted as a byte.
var bytes = window.atob(str.replace(/-/g, '+').replace(/_/g, '/'));
var result = new Uint8Array(bytes.length);
for (var i = 0; i < bytes.length; ++i) {
result[i] = bytes.charCodeAt(i);
}
return result;
};
/**
* Convert a hex string to a Uint8Array.
* @param {string} str
* @return {!Uint8Array}
*/
shaka.util.Uint8ArrayUtils.fromHex = function(str) {
var arr = new Uint8Array(str.length / 2);
for (var i = 0; i < str.length; i += 2) {
arr[i / 2] = window.parseInt(str.substr(i, 2), 16);
}
return arr;
};
/**
* Convert a Uint8Array to a hex string.
* @param {!Uint8Array} arr
* @return {string}
*/
shaka.util.Uint8ArrayUtils.toHex = function(arr) {
var hex = '';
for (var i = 0; i < arr.length; ++i) {
var value = arr[i].toString(16);
if (value.length == 1) value = '0' + value;
hex += value;
}
return hex;
};
/**
* Compare two Uint8Arrays for equality.
* @param {Uint8Array} arr1
* @param {Uint8Array} arr2
* @return {boolean}
*/
shaka.util.Uint8ArrayUtils.equal = function(arr1, arr2) {
if (!arr1 && !arr2) return true;
if (!arr1 || !arr2) return false;
if (arr1.length != arr2.length) return false;
for (var i = 0; i < arr1.length; ++i) {
if (arr1[i] != arr2[i]) return false;
}
return true;
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | 2 | /**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.util.XmlUtils');
goog.require('goog.asserts');
goog.require('shaka.log');
/**
* @namespace shaka.util.XmlUtils
* @summary A set of XML utility functions.
*/
/**
* Finds a child XML element.
* @param {!Element} elem The parent XML element.
* @param {string} name The child XML element's tag name.
* @return {Element} The child XML element, or null if a child XML element does
* not exist with the given tag name OR if there exists more than one
* child XML element with the given tag name.
*/
shaka.util.XmlUtils.findChild = function(elem, name) {
var children = shaka.util.XmlUtils.findChildren(elem, name);
if (children.length != 1)
return null;
return children[0];
};
/**
* Finds child XML elements.
* @param {!Element} elem The parent XML element.
* @param {string} name The child XML element's tag name.
* @return {!Array.<!Element>} The child XML elements.
*/
shaka.util.XmlUtils.findChildren = function(elem, name) {
return Array.prototype.filter.call(elem.childNodes, function(child) {
goog.asserts.assert(
child.tagName != name || child instanceof Element,
'child element should be an Element');
return child.tagName == name;
});
};
/**
* Gets the text contents of a node.
* @param {!Element} elem The XML element.
* @return {?string} The text contents, or null if there are none.
*/
shaka.util.XmlUtils.getContents = function(elem) {
var contents = elem.firstChild;
// check content
if (!contents || contents.nodeType != Node.TEXT_NODE)
return null;
// read merged text content from all text nodes (fixes MSIE 11 bug)
return elem.textContent.trim();
};
/**
* Parses an attribute by its name.
* @param {!Element} elem The XML element.
* @param {string} name The attribute name.
* @param {function(string): (T|null)} parseFunction A function that parses
* the attribute.
* @param {(T|null)=} opt_defaultValue The attribute's default value, if not
* specified, the attibute's default value is null.
* @return {(T|null)} The parsed attribute on success, or the attribute's
* default value if the attribute does not exist or could not be parsed.
* @template T
*/
shaka.util.XmlUtils.parseAttr = function(
elem, name, parseFunction, opt_defaultValue) {
var parsedValue = null;
var value = elem.getAttribute(name);
if (value != null)
parsedValue = parseFunction(value);
if (parsedValue == null)
return opt_defaultValue !== undefined ? opt_defaultValue : null;
return parsedValue;
};
/**
* Parses an XML date string.
* @param {string} dateString
* @return {?number} The parsed date in seconds on success; otherwise, return
* null.
*/
shaka.util.XmlUtils.parseDate = function(dateString) {
if (!dateString)
return null;
var result = Date.parse(dateString);
return (!isNaN(result) ? Math.floor(result / 1000.0) : null);
};
/**
* Parses an XML duration string.
* Negative values are not supported. Years and months are treated as exactly
* 365 and 30 days respectively.
* @param {string} durationString The duration string, e.g., "PT1H3M43.2S",
* which means 1 hour, 3 minutes, and 43.2 seconds.
* @return {?number} The parsed duration in seconds on success; otherwise,
* return null.
* @see {@link http://www.datypic.com/sc/xsd/t-xsd_duration.html}
*/
shaka.util.XmlUtils.parseDuration = function(durationString) {
if (!durationString)
return null;
var re = '^P(?:([0-9]*)Y)?(?:([0-9]*)M)?(?:([0-9]*)D)?' +
'(?:T(?:([0-9]*)H)?(?:([0-9]*)M)?(?:([0-9.]*)S)?)?$';
var matches = new RegExp(re).exec(durationString);
if (!matches) {
shaka.log.warning('Invalid duration string:', durationString);
return null;
}
// Note: Number(null) == 0 but Number(undefined) == NaN.
var years = Number(matches[1] || null);
var months = Number(matches[2] || null);
var days = Number(matches[3] || null);
var hours = Number(matches[4] || null);
var minutes = Number(matches[5] || null);
var seconds = Number(matches[6] || null);
// Assume a year always has 365 days and a month always has 30 days.
var d = (60 * 60 * 24 * 365) * years +
(60 * 60 * 24 * 30) * months +
(60 * 60 * 24) * days +
(60 * 60) * hours +
60 * minutes +
seconds;
return isFinite(d) ? d : null;
};
/**
* Parses a range string.
* @param {string} rangeString The range string, e.g., "101-9213".
* @return {?{start: number, end: number}} The parsed range on success;
* otherwise, return null.
*/
shaka.util.XmlUtils.parseRange = function(rangeString) {
var matches = /([0-9]+)-([0-9]+)/.exec(rangeString);
if (!matches)
return null;
var start = Number(matches[1]);
if (!isFinite(start))
return null;
var end = Number(matches[2]);
if (!isFinite(end))
return null;
return {start: start, end: end};
};
/**
* Parses an integer.
* @param {string} intString The integer string.
* @return {?number} The parsed integer on success; otherwise, return null.
*/
shaka.util.XmlUtils.parseInt = function(intString) {
var n = Number(intString);
return (n % 1 === 0) ? n : null;
};
/**
* Parses a positive integer.
* @param {string} intString The integer string.
* @return {?number} The parsed positive integer on success; otherwise,
* return null.
*/
shaka.util.XmlUtils.parsePositiveInt = function(intString) {
var n = Number(intString);
return (n % 1 === 0) && (n > 0) ? n : null;
};
/**
* Parses a non-negative integer.
* @param {string} intString The integer string.
* @return {?number} The parsed non-negative integer on success; otherwise,
* return null.
*/
shaka.util.XmlUtils.parseNonNegativeInt = function(intString) {
var n = Number(intString);
return (n % 1 === 0) && (n >= 0) ? n : null;
};
/**
* Parses a floating point number.
* @param {string} floatString The floating point number string.
* @return {?number} The parsed floating point number on success; otherwise,
* return null. May return -Infinity or Infinity.
*/
shaka.util.XmlUtils.parseFloat = function(floatString) {
var n = Number(floatString);
return !isNaN(n) ? n : null;
};
/**
* Evaluate a division expressed as a string
* @param {string} exprString
* The expression to evaluate, e.g. "200/2". Can also be a single number
* @return {?number} The evaluated expression as floating point number on
* success; otherwise return null.
*/
shaka.util.XmlUtils.evalDivision = function(exprString) {
var res;
var n;
if (res = exprString.match(/^(\d+)\/(\d+)$/)) {
n = Number(res[1] / res[2]);
} else {
n = Number(exprString);
}
return !isNaN(n) ? n : null;
};
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| base.js | 28.34% | (53 / 187) | 23.58% | (25 / 106) | 12% | (3 / 25) | 28.34% | (53 / 187) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 | 1 1 1 1 1 1 4 1 4 4 4 4 8 4 4 3 1 1 4 4 4 4 4 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 | // Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Bootstrap for the Google JS Library (Closure).
*
* In uncompiled mode base.js will write out Closure's deps file, unless the
* global <code>CLOSURE_NO_DEPS</code> is set to true. This allows projects to
* include their own deps file(s) from different locations.
*
*
* @provideGoog
*/
/**
* @define {boolean} Overridden to true by the compiler when --closure_pass
* or --mark_as_compiled is specified.
*/
var COMPILED = false;
/**
* Base namespace for the Closure library. Checks to see goog is already
* defined in the current scope before assigning to prevent clobbering if
* base.js is loaded more than once.
*
* @const
*/
var goog = goog || {};
/**
* Reference to the global context. In most cases this will be 'window'.
*/
goog.global = this;
/**
* A hook for overriding the define values in uncompiled mode.
*
* In uncompiled mode, {@code CLOSURE_UNCOMPILED_DEFINES} may be defined before
* loading base.js. If a key is defined in {@code CLOSURE_UNCOMPILED_DEFINES},
* {@code goog.define} will use the value instead of the default value. This
* allows flags to be overwritten without compilation (this is normally
* accomplished with the compiler's "define" flag).
*
* Example:
* <pre>
* var CLOSURE_UNCOMPILED_DEFINES = {'goog.DEBUG': false};
* </pre>
*
* @type {Object.<string, (string|number|boolean)>|undefined}
*/
goog.global.CLOSURE_UNCOMPILED_DEFINES;
/**
* A hook for overriding the define values in uncompiled or compiled mode,
* like CLOSURE_UNCOMPILED_DEFINES but effective in compiled code. In
* uncompiled code CLOSURE_UNCOMPILED_DEFINES takes precedence.
*
* Also unlike CLOSURE_UNCOMPILED_DEFINES the values must be number, boolean or
* string literals or the compiler will emit an error.
*
* While any @define value may be set, only those set with goog.define will be
* effective for uncompiled code.
*
* Example:
* <pre>
* var CLOSURE_DEFINES = {'goog.DEBUG': false};
* </pre>
*
* @type {Object.<string, (string|number|boolean)>|undefined}
*/
goog.global.CLOSURE_DEFINES;
/**
* Returns true if the specified value is not undefined.
* WARNING: Do not use this to test if an object has a property. Use the in
* operator instead.
*
* @param {?} val Variable to test.
* @return {boolean} Whether variable is defined.
*/
goog.isDef = function(val) {
// void 0 always evaluates to undefined and hence we do not need to depend on
// the definition of the global variable named 'undefined'.
return val !== void 0;
};
/**
* Builds an object structure for the provided namespace path, ensuring that
* names that already exist are not overwritten. For example:
* "a.b.c" -> a = {};a.b={};a.b.c={};
* Used by goog.provide and goog.exportSymbol.
* @param {string} name name of the object that this file defines.
* @param {*=} opt_object the object to expose at the end of the path.
* @param {Object=} opt_objectToExportTo The object to add the path to; default
* is |goog.global|.
* @private
*/
goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) {
var parts = name.split('.');
var cur = opt_objectToExportTo || goog.global;
// Internet Explorer exhibits strange behavior when throwing errors from
// methods externed in this manner. See the testExportSymbolExceptions in
// base_test.html for an example.
Iif (!(parts[0] in cur) && cur.execScript) {
cur.execScript('var ' + parts[0]);
}
// Certain browsers cannot parse code in the form for((a in b); c;);
// This pattern is produced by the JSCompiler when it collapses the
// statement above into the conditional loop below. To prevent this from
// happening, use a for-loop and reserve the init logic as below.
// Parentheses added to eliminate strict JS warning in Firefox.
for (var part; parts.length && (part = parts.shift());) {
if (!parts.length && goog.isDef(opt_object)) {
// last part and we have an object; use it
cur[part] = opt_object;
} else if (cur[part]) {
cur = cur[part];
} else {
cur = cur[part] = {};
}
}
};
/**
* Defines a named value. In uncompiled mode, the value is retreived from
* CLOSURE_DEFINES or CLOSURE_UNCOMPILED_DEFINES if the object is defined and
* has the property specified, and otherwise used the defined defaultValue.
* When compiled, the default can be overridden using compiler command-line
* options.
*
* @param {string} name The distinguished name to provide.
* @param {string|number|boolean} defaultValue
*/
goog.define = function(name, defaultValue) {
var value = defaultValue;
Eif (!COMPILED) {
Iif (goog.global.CLOSURE_UNCOMPILED_DEFINES &&
Object.prototype.hasOwnProperty.call(
goog.global.CLOSURE_UNCOMPILED_DEFINES, name)) {
value = goog.global.CLOSURE_UNCOMPILED_DEFINES[name];
} else Iif (goog.global.CLOSURE_DEFINES &&
Object.prototype.hasOwnProperty.call(
goog.global.CLOSURE_DEFINES, name)) {
value = goog.global.CLOSURE_DEFINES[name];
}
}
goog.exportPath_(name, value);
};
/**
* @define {boolean} DEBUG is provided as a convenience so that debugging code
* that should not be included in a production js_binary can be easily stripped
* by specifying --define goog.DEBUG=false to the JSCompiler. For example, most
* toString() methods should be declared inside an "if (goog.DEBUG)" conditional
* because they are generally used for debugging purposes and it is difficult
* for the JSCompiler to statically determine whether they are used.
*/
goog.DEBUG = true;
/**
* @define {string} LOCALE defines the locale being used for compilation. It is
* used to select locale specific data to be compiled in js binary. BUILD rule
* can specify this value by "--define goog.LOCALE=<locale_name>" as JSCompiler
* option.
*
* Take into account that the locale code format is important. You should use
* the canonical Unicode format with hyphen as a delimiter. Language must be
* lowercase, Language Script - Capitalized, Region - UPPERCASE.
* There are few examples: pt-BR, en, en-US, sr-Latin-BO, zh-Hans-CN.
*
* See more info about locale codes here:
* http://www.unicode.org/reports/tr35/#Unicode_Language_and_Locale_Identifiers
*
* For language codes you should use values defined by ISO 693-1. See it here
* http://www.w3.org/WAI/ER/IG/ert/iso639.htm. There is only one exception from
* this rule: the Hebrew language. For legacy reasons the old code (iw) should
* be used instead of the new code (he), see http://wiki/Main/IIISynonyms.
*/
goog.define('goog.LOCALE', 'en'); // default to en
/**
* @define {boolean} Whether this code is running on trusted sites.
*
* On untrusted sites, several native functions can be defined or overridden by
* external libraries like Prototype, Datejs, and JQuery and setting this flag
* to false forces closure to use its own implementations when possible.
*
* If your JavaScript can be loaded by a third party site and you are wary about
* relying on non-standard implementations, specify
* "--define goog.TRUSTED_SITE=false" to the JSCompiler.
*/
goog.define('goog.TRUSTED_SITE', true);
/**
* @define {boolean} Whether a project is expected to be running in strict mode.
*
* This define can be used to trigger alternate implementations compatible with
* running in EcmaScript Strict mode or warn about unavailable functionality.
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/Strict_mode
*/
goog.define('goog.STRICT_MODE_COMPATIBLE', false);
/**
* Creates object stubs for a namespace. The presence of one or more
* goog.provide() calls indicate that the file defines the given
* objects/namespaces. Provided objects must not be null or undefined.
* Build tools also scan for provide/require statements
* to discern dependencies, build dependency files (see deps.js), etc.
* @see goog.require
* @param {string} name Namespace provided by this file in the form
* "goog.package.part".
*/
goog.provide = function(name) {
if (!COMPILED) {
// Ensure that the same namespace isn't provided twice.
// A goog.module/goog.provide maps a goog.require to a specific file
if (goog.isProvided_(name)) {
throw Error('Namespace "' + name + '" already declared.');
}
delete goog.implicitNamespaces_[name];
var namespace = name;
while ((namespace = namespace.substring(0, namespace.lastIndexOf('.')))) {
if (goog.getObjectByName(namespace)) {
break;
}
goog.implicitNamespaces_[namespace] = true;
}
}
goog.exportPath_(name);
};
/**
* Forward declares a symbol. This is an indication to the compiler that the
* symbol may be used in the source yet is not required and may not be provided
* in compilation.
*
* The most common usage of forward declaration is code that takes a type as a
* function parameter but does not need to require it. By forward declaring
* instead of requiring, no hard dependency is made, and (if not required
* elsewhere) the namespace may never be required and thus, not be pulled
* into the JavaScript binary. If it is required elsewhere, it will be type
* checked as normal.
*
*
* @param {string} name The namespace to forward declare in the form of
* "goog.package.part".
*/
goog.forwardDeclare = function(name) {};
Eif (!COMPILED) {
/**
* Check if the given name has been goog.provided. This will return false for
* names that are available only as implicit namespaces.
* @param {string} name name of the object to look for.
* @return {boolean} Whether the name has been provided.
* @private
*/
goog.isProvided_ = function(name) {
return (!goog.implicitNamespaces_[name] &&
goog.isDefAndNotNull(goog.getObjectByName(name)));
};
/**
* Namespaces implicitly defined by goog.provide. For example,
* goog.provide('goog.events.Event') implicitly declares that 'goog' and
* 'goog.events' must be namespaces.
*
* @type {Object.<string, (boolean|undefined)>}
* @private
*/
goog.implicitNamespaces_ = {};
}
/**
* Returns an object based on its fully qualified external name. The object
* is not found if null or undefined. If you are using a compilation pass that
* renames property names beware that using this function will not find renamed
* properties.
*
* @param {string} name The fully qualified name.
* @param {Object=} opt_obj The object within which to look; default is
* |goog.global|.
* @return {?} The value (object or primitive) or, if not found, null.
*/
goog.getObjectByName = function(name, opt_obj) {
var parts = name.split('.');
var cur = opt_obj || goog.global;
for (var part; part = parts.shift(); ) {
if (goog.isDefAndNotNull(cur[part])) {
cur = cur[part];
} else {
return null;
}
}
return cur;
};
/**
* Globalizes a whole namespace, such as goog or goog.lang.
*
* @param {Object} obj The namespace to globalize.
* @param {Object=} opt_global The object to add the properties to.
* @deprecated Properties may be explicitly exported to the global scope, but
* this should no longer be done in bulk.
*/
goog.globalize = function(obj, opt_global) {
var global = opt_global || goog.global;
for (var x in obj) {
global[x] = obj[x];
}
};
/**
* Adds a dependency from a file to the files it requires.
* @param {string} relPath The path to the js file.
* @param {Array} provides An array of strings with the names of the objects
* this file provides.
* @param {Array} requires An array of strings with the names of the objects
* this file requires.
*/
goog.addDependency = function(relPath, provides, requires) {
if (goog.DEPENDENCIES_ENABLED) {
var provide, require;
var path = relPath.replace(/\\/g, '/');
var deps = goog.dependencies_;
for (var i = 0; provide = provides[i]; i++) {
deps.nameToPath[provide] = path;
}
for (var j = 0; require = requires[j]; j++) {
if (!(path in deps.requires)) {
deps.requires[path] = {};
}
deps.requires[path][require] = true;
}
}
};
// NOTE(nnaze): The debug DOM loader was included in base.js as an original way
// to do "debug-mode" development. The dependency system can sometimes be
// confusing, as can the debug DOM loader's asynchronous nature.
//
// With the DOM loader, a call to goog.require() is not blocking -- the script
// will not load until some point after the current script. If a namespace is
// needed at runtime, it needs to be defined in a previous script, or loaded via
// require() with its registered dependencies.
// User-defined namespaces may need their own deps file. See http://go/js_deps,
// http://go/genjsdeps, or, externally, DepsWriter.
// https://developers.google.com/closure/library/docs/depswriter
//
// Because of legacy clients, the DOM loader can't be easily removed from
// base.js. Work is being done to make it disableable or replaceable for
// different environments (DOM-less JavaScript interpreters like Rhino or V8,
// for example). See bootstrap/ for more information.
/**
* @define {boolean} Whether to enable the debug loader.
*
* If enabled, a call to goog.require() will attempt to load the namespace by
* appending a script tag to the DOM (if the namespace has been registered).
*
* If disabled, goog.require() will simply assert that the namespace has been
* provided (and depend on the fact that some outside tool correctly ordered
* the script).
*/
goog.define('goog.ENABLE_DEBUG_LOADER', true);
/**
* @param {string} msg
* @private
*/
goog.logToConsole_ = function(msg) {
if (goog.global.console) {
goog.global.console['error'](msg);
}
};
/**
* Implements a system for the dynamic resolution of dependencies that works in
* parallel with the BUILD system. Note that all calls to goog.require will be
* stripped by the JSCompiler when the --closure_pass option is used.
* @see goog.provide
* @param {string} name Namespace to include (as was given in goog.provide()) in
* the form "goog.package.part".
* @return {?} If called within a goog.module file, the associated namespace or
* module otherwise null.
*/
goog.require = function(name) {
// If the object already exists we do not need do do anything.
if (!COMPILED) {
if (goog.isProvided_(name)) {
return null;
}
if (goog.ENABLE_DEBUG_LOADER) {
var path = goog.getPathFromDeps_(name);
if (path) {
goog.included_[path] = true;
goog.writeScripts_();
return null;
}
}
var errorMessage = 'goog.require could not find: ' + name;
goog.logToConsole_(errorMessage);
throw Error(errorMessage);
}
};
/**
* Path for included scripts.
* @type {string}
*/
goog.basePath = '';
/**
* A hook for overriding the base path.
* @type {string|undefined}
*/
goog.global.CLOSURE_BASE_PATH;
/**
* Whether to load Closure's deps file automatically.
* Shaka sets this to true since we are placing deps.js in a non-standard
* location. Running Shaka in uncompiled mode will require loading deps.js
* explicitly.
* @type {boolean|undefined}
*/
goog.global.CLOSURE_NO_DEPS = true;
/**
* A function to import a single script. This is meant to be overridden when
* Closure is being run in non-HTML contexts, such as web workers. It's defined
* in the global scope so that it can be set before base.js is loaded, which
* allows deps.js to be imported properly.
*
* The function is passed the script source, which is a relative URI. It should
* return true if the script was imported, false otherwise.
* @type {(function(string): boolean)|undefined}
*/
goog.global.CLOSURE_IMPORT_SCRIPT;
/**
* True if goog.dependencies_ is available.
* @const {boolean}
*/
goog.DEPENDENCIES_ENABLED = !COMPILED && goog.ENABLE_DEBUG_LOADER;
Iif (goog.DEPENDENCIES_ENABLED) {
/**
* Object used to keep track of urls that have already been added. This record
* allows the prevention of circular dependencies.
* @type {Object}
* @private
*/
goog.included_ = {};
/**
* This object is used to keep track of dependencies and other data that is
* used for loading scripts.
* @private
* @type {Object}
*/
goog.dependencies_ = {
nameToPath: {}, // many to 1
requires: {}, // 1 to many
// Used when resolving dependencies to prevent us from visiting file twice.
visited: {},
written: {} // Used to keep track of script files we have written.
};
/**
* Tries to detect whether is in the context of an HTML document.
* @return {boolean} True if it looks like HTML document.
* @private
*/
goog.inHtmlDocument_ = function() {
var doc = goog.global.document;
return typeof doc != 'undefined' &&
'write' in doc; // XULDocument misses write.
};
/**
* Tries to detect the base path of base.js script that bootstraps Closure.
* @private
*/
goog.findBasePath_ = function() {
if (goog.global.CLOSURE_BASE_PATH) {
goog.basePath = goog.global.CLOSURE_BASE_PATH;
return;
} else if (!goog.inHtmlDocument_()) {
return;
}
var doc = goog.global.document;
var scripts = doc.getElementsByTagName('script');
// Search backwards since the current script is in almost all cases the one
// that has base.js.
for (var i = scripts.length - 1; i >= 0; --i) {
var src = scripts[i].src;
var qmark = src.lastIndexOf('?');
var l = qmark == -1 ? src.length : qmark;
if (src.substr(l - 7, 7) == 'base.js') {
goog.basePath = src.substr(0, l - 7);
return;
}
}
};
/**
* Imports a script if, and only if, that script hasn't already been imported.
* (Must be called at execution time)
* @param {string} src Script source.
* @param {string=} opt_sourceText The optionally source text to evaluate
* @private
*/
goog.importScript_ = function(src, opt_sourceText) {
var importScript = goog.global.CLOSURE_IMPORT_SCRIPT ||
goog.writeScriptTag_;
if (importScript(src, opt_sourceText)) {
goog.dependencies_.written[src] = true;
}
};
/**
* The default implementation of the import function. Writes a script tag to
* import the script.
*
* @param {string} src The script url.
* @param {string=} opt_sourceText The optionally source text to evaluate
* @return {boolean} True if the script was imported, false otherwise.
* @private
*/
goog.writeScriptTag_ = function(src, opt_sourceText) {
if (goog.inHtmlDocument_()) {
var doc = goog.global.document;
// If the user tries to require a new symbol after document load,
// something has gone terribly wrong. Doing a document.write would
// wipe out the page.
if (doc.readyState == 'complete') {
// Certain test frameworks load base.js multiple times, which tries
// to write deps.js each time. If that happens, just fail silently.
// These frameworks wipe the page between each load of base.js, so this
// is OK.
var isDeps = /\bdeps.js$/.test(src);
if (isDeps) {
return false;
} else {
throw Error('Cannot write "' + src + '" after document load');
}
}
if (opt_sourceText === undefined) {
doc.write(
'<script type="text/javascript" src="' +
src + '"></' + 'script>');
} else {
doc.write(
'<script type="text/javascript">' +
opt_sourceText + '</' + 'script>');
}
return true;
} else {
return false;
}
};
/**
* Resolves dependencies based on the dependencies added using addDependency
* and calls importScript_ in the correct order.
* @private
*/
goog.writeScripts_ = function() {
// The scripts we need to write this time.
var scripts = [];
var seenScript = {};
var deps = goog.dependencies_;
function visitNode(path) {
if (path in deps.written) {
return;
}
// We have already visited this one. We can get here if we have cyclic
// dependencies.
if (path in deps.visited) {
if (!(path in seenScript)) {
seenScript[path] = true;
scripts.push(path);
}
return;
}
deps.visited[path] = true;
if (path in deps.requires) {
for (var requireName in deps.requires[path]) {
// If the required name is defined, we assume that it was already
// bootstrapped by other means.
if (!goog.isProvided_(requireName)) {
if (requireName in deps.nameToPath) {
visitNode(deps.nameToPath[requireName]);
} else {
throw Error('Undefined nameToPath for ' + requireName);
}
}
}
}
if (!(path in seenScript)) {
seenScript[path] = true;
scripts.push(path);
}
}
for (var path in goog.included_) {
if (!deps.written[path]) {
visitNode(path);
}
}
// record that we are going to load all these scripts.
for (var i = 0; i < scripts.length; i++) {
var path = scripts[i];
goog.dependencies_.written[path] = true;
}
for (var i = 0; i < scripts.length; i++) {
var path = scripts[i];
if (path) {
goog.importScript_(goog.basePath + path);
}
}
};
/**
* Looks at the dependency rules and tries to determine the script file that
* fulfills a particular rule.
* @param {string} rule In the form goog.namespace.Class or project.script.
* @return {?string} Url corresponding to the rule, or null.
* @private
*/
goog.getPathFromDeps_ = function(rule) {
if (rule in goog.dependencies_.nameToPath) {
return goog.dependencies_.nameToPath[rule];
} else {
return null;
}
};
goog.findBasePath_();
// Allow projects to manage the deps files themselves.
if (!goog.global.CLOSURE_NO_DEPS) {
goog.importScript_(goog.basePath + 'deps.js');
}
}
//==============================================================================
// Language Enhancements
//==============================================================================
/**
* Returns true if the specified value is defined and not null.
* @param {?} val Variable to test.
* @return {boolean} Whether variable is defined and not null.
*/
goog.isDefAndNotNull = function(val) {
// Note that undefined == null.
return val != null;
};
/**
* Returns true if the specified value is a string.
* @param {?} val Variable to test.
* @return {boolean} Whether variable is a string.
*/
goog.isString = function(val) {
return typeof val == 'string';
};
/**
* Exposes an unobfuscated global namespace path for the given object.
* Note that fields of the exported object *will* be obfuscated, unless they are
* exported in turn via this function or goog.exportProperty.
*
* Also handy for making public items that are defined in anonymous closures.
*
* ex. goog.exportSymbol('public.path.Foo', Foo);
*
* ex. goog.exportSymbol('public.path.Foo.staticFunction', Foo.staticFunction);
* public.path.Foo.staticFunction();
*
* ex. goog.exportSymbol('public.path.Foo.prototype.myMethod',
* Foo.prototype.myMethod);
* new public.path.Foo().myMethod();
*
* @param {string} publicPath Unobfuscated name to export.
* @param {*} object Object the name should point to.
* @param {Object=} opt_objectToExportTo The object to add the path to; default
* is goog.global.
*/
goog.exportSymbol = function(publicPath, object, opt_objectToExportTo) {
goog.exportPath_(publicPath, object, opt_objectToExportTo);
};
/**
* Exports a property unobfuscated into the object's namespace.
* ex. goog.exportProperty(Foo, 'staticFunction', Foo.staticFunction);
* ex. goog.exportProperty(Foo.prototype, 'myMethod', Foo.prototype.myMethod);
* @param {Object} object Object whose static property is being exported.
* @param {string} publicName Unobfuscated name to export.
* @param {*} symbol Object the name should point to.
*/
goog.exportProperty = function(object, publicName, symbol) {
object[publicName] = symbol;
};
/**
* Inherit the prototype methods from one constructor into another.
*
* Usage:
* <pre>
* function ParentClass(a, b) { }
* ParentClass.prototype.foo = function(a) { };
*
* function ChildClass(a, b, c) {
* ChildClass.base(this, 'constructor', a, b);
* }
* goog.inherits(ChildClass, ParentClass);
*
* var child = new ChildClass('a', 'b', 'see');
* child.foo(); // This works.
* </pre>
*
* @param {Function} childCtor Child class.
* @param {Function} parentCtor Parent class.
*/
goog.inherits = function(childCtor, parentCtor) {
/** @constructor */
function tempCtor() {}
tempCtor.prototype = parentCtor.prototype;
childCtor.superClass_ = parentCtor.prototype;
childCtor.prototype = new tempCtor();
/** @override */
childCtor.prototype.constructor = childCtor;
/**
* Calls superclass constructor/method.
*
* This function is only available if you use goog.inherits to
* express inheritance relationships between classes.
*
* NOTE: This is a replacement for goog.base and for superClass_
* property defined in childCtor.
*
* @param {!Object} me Should always be "this".
* @param {string} methodName The method name to call. Calling
* superclass constructor can be done with the special string
* 'constructor'.
* @param {...*} var_args The arguments to pass to superclass
* method/constructor.
* @return {*} The return value of the superclass method/constructor.
*/
childCtor.base = function(me, methodName, var_args) {
var args = Array.prototype.slice.call(arguments, 2);
return parentCtor.prototype[methodName].apply(me, args);
};
};
/*
* To support uncompiled, strict mode bundles that use eval to divide source
* like so:
* eval('someSource;//# sourceUrl sourcefile.js');
* We need to export the globally defined symbols "goog" and "COMPILED".
* Exporting "goog" breaks the compiler optimizations, so we required that
* be defined externally.
* NOTE: We don't use goog.exportSymbol here because we don't want to trigger
* extern generation when that compiler option is enabled.
*/
Eif (!COMPILED) {
goog.global['COMPILED'] = COMPILED;
}
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 | 2 2 1 1 1 1 1 1 | /*global java */
/*eslint no-process-exit:0 */
/**
* Helper methods for running JSDoc on the command line.
*
* A few critical notes for anyone who works on this module:
*
* + The module should really export an instance of `cli`, and `props` should be properties of a
* `cli` instance. However, Rhino interpreted `this` as a reference to `global` within the
* prototype's methods, so we couldn't do that.
* + On Rhino, for unknown reasons, the `jsdoc/fs` and `jsdoc/path` modules can fail in some cases
* when they are required by this module. You may need to use `fs` and `path` instead.
*
* @private
*/
module.exports = (function() {
'use strict';
var logger = require('jsdoc/util/logger');
var stripJsonComments = require('strip-json-comments');
var hasOwnProp = Object.prototype.hasOwnProperty;
var props = {
docs: [],
packageJson: null,
shouldExitWithError: false,
tmpdir: null
};
var app = global.app;
var env = global.env;
var FATAL_ERROR_MESSAGE = 'Exiting JSDoc because an error occurred. See the previous log ' +
'messages for details.';
var cli = {};
// TODO: docs
cli.setVersionInfo = function() {
var fs = require('fs');
var path = require('path');
// allow this to throw--something is really wrong if we can't read our own package file
var info = JSON.parse( fs.readFileSync(path.join(env.dirname, 'package.json'), 'utf8') );
env.version = {
number: info.version,
revision: new Date( parseInt(info.revision, 10) ).toUTCString()
};
return cli;
};
// TODO: docs
cli.loadConfig = function() {
var _ = require('underscore');
var args = require('jsdoc/opts/args');
var Config = require('jsdoc/config');
var fs = require('jsdoc/fs');
var path = require('jsdoc/path');
var confPath;
var isFile;
var defaultOpts = {
destination: './out/',
encoding: 'utf8'
};
try {
env.opts = args.parse(env.args);
}
catch (e) {
cli.exit(1, e.message + '\n' + FATAL_ERROR_MESSAGE);
}
confPath = env.opts.configure || path.join(env.dirname, 'conf.json');
try {
isFile = fs.statSync(confPath).isFile();
}
catch(e) {
isFile = false;
}
if ( !isFile && !env.opts.configure ) {
confPath = path.join(env.dirname, 'conf.json.EXAMPLE');
}
try {
env.conf = new Config( stripJsonComments(fs.readFileSync(confPath, 'utf8')) )
.get();
}
catch (e) {
cli.exit(1, 'Cannot parse the config file ' + confPath + ': ' + e + '\n' +
FATAL_ERROR_MESSAGE);
}
// look for options on the command line, in the config file, and in the defaults, in that order
env.opts = _.defaults(env.opts, env.conf.opts, defaultOpts);
return cli;
};
// TODO: docs
cli.configureLogger = function() {
function recoverableError() {
props.shouldExitWithError = true;
}
function fatalError() {
cli.exit(1);
}
if (env.opts.debug) {
logger.setLevel(logger.LEVELS.DEBUG);
}
else if (env.opts.verbose) {
logger.setLevel(logger.LEVELS.INFO);
}
if (env.opts.pedantic) {
logger.once('logger:warn', recoverableError);
logger.once('logger:error', fatalError);
}
else {
logger.once('logger:error', recoverableError);
}
logger.once('logger:fatal', fatalError);
return cli;
};
// TODO: docs
cli.logStart = function() {
logger.debug( cli.getVersion() );
logger.debug('Environment info: %j', {
env: {
conf: env.conf,
opts: env.opts
}
});
};
// TODO: docs
cli.logFinish = function() {
var delta;
var deltaSeconds;
if (env.run.finish && env.run.start) {
delta = env.run.finish.getTime() - env.run.start.getTime();
}
if (delta !== undefined) {
deltaSeconds = (delta / 1000).toFixed(2);
logger.info('Finished running in %s seconds.', deltaSeconds);
}
};
// TODO: docs
cli.runCommand = function(cb) {
var cmd;
var opts = env.opts;
function done(errorCode) {
if (!errorCode && props.shouldExitWithError) {
cb(1);
}
else {
cb(errorCode);
}
}
if (opts.help) {
cmd = cli.printHelp;
}
else if (opts.test) {
cmd = cli.runTests;
}
else if (opts.version) {
cmd = cli.printVersion;
}
else {
cmd = cli.main;
}
cmd(done);
};
// TODO: docs
cli.printHelp = function(cb) {
cli.printVersion();
console.log( '\n' + require('jsdoc/opts/args').help() + '\n' );
console.log('Visit http://usejsdoc.org for more information.');
cb(0);
};
// TODO: docs
cli.runTests = function(cb) {
var path = require('jsdoc/path');
var runner = require( path.join(env.dirname, 'test/runner') );
console.log('Running tests...');
runner(function(failCount) {
cb(failCount);
});
};
// TODO: docs
cli.getVersion = function() {
return 'JSDoc ' + env.version.number + ' (' + env.version.revision + ')';
};
// TODO: docs
cli.printVersion = function(cb) {
console.log( cli.getVersion() );
if (cb) {
cb(0);
}
};
// TODO: docs
cli.main = function(cb) {
cli.scanFiles();
if (env.sourceFiles.length) {
cli.createParser()
.parseFiles()
.processParseResults();
}
else {
console.log('There are no input files to process.\n');
cli.printHelp(cb);
}
env.run.finish = new Date();
cb(0);
};
function readPackageJson(filepath) {
var fs = require('jsdoc/fs');
try {
return stripJsonComments( fs.readFileSync(filepath, 'utf8') );
}
catch (e) {
logger.error('Unable to read the package file "%s"', filepath);
return null;
}
}
function buildSourceList() {
var fs = require('jsdoc/fs');
var Readme = require('jsdoc/readme');
var packageJson;
var readmeHtml;
var sourceFile;
var sourceFiles = env.opts._ ? env.opts._.slice(0) : [];
if (env.conf.source && env.conf.source.include) {
sourceFiles = sourceFiles.concat(env.conf.source.include);
}
// load the user-specified package/README files, if any
if (env.opts.package) {
packageJson = readPackageJson(env.opts.package);
}
if (env.opts.readme) {
readmeHtml = new Readme(env.opts.readme).html;
}
// source files named `package.json` or `README.md` get special treatment, unless the user
// explicitly specified a package and/or README file
for (var i = 0, l = sourceFiles.length; i < l; i++) {
sourceFile = sourceFiles[i];
if ( !env.opts.package && /\bpackage\.json$/i.test(sourceFile) ) {
packageJson = readPackageJson(sourceFile);
sourceFiles.splice(i--, 1);
}
if ( !env.opts.readme && /(\bREADME|\.md)$/i.test(sourceFile) ) {
readmeHtml = new Readme(sourceFile).html;
sourceFiles.splice(i--, 1);
}
}
props.packageJson = packageJson;
env.opts.readme = readmeHtml;
return sourceFiles;
}
// TODO: docs
cli.scanFiles = function() {
var Filter = require('jsdoc/src/filter').Filter;
var filter;
env.opts._ = buildSourceList();
// are there any files to scan and parse?
if (env.conf.source && env.opts._.length) {
filter = new Filter(env.conf.source);
env.sourceFiles = app.jsdoc.scanner.scan(env.opts._, (env.opts.recurse ? 10 : undefined),
filter);
}
return cli;
};
function resolvePluginPaths(paths) {
var path = require('jsdoc/path');
var pluginPaths = [];
paths.forEach(function(plugin) {
var basename = path.basename(plugin);
var dirname = path.dirname(plugin);
var pluginPath = path.getResourcePath(dirname);
if (!pluginPath) {
logger.error('Unable to find the plugin "%s"', plugin);
return;
}
pluginPaths.push( path.join(pluginPath, basename) );
});
return pluginPaths;
}
cli.createParser = function() {
var handlers = require('jsdoc/src/handlers');
var parser = require('jsdoc/src/parser');
var path = require('jsdoc/path');
var plugins = require('jsdoc/plugins');
app.jsdoc.parser = parser.createParser(env.conf.parser);
if (env.conf.plugins) {
env.conf.plugins = resolvePluginPaths(env.conf.plugins);
plugins.installPlugins(env.conf.plugins, app.jsdoc.parser);
}
handlers.attachTo(app.jsdoc.parser);
return cli;
};
cli.parseFiles = function() {
var augment = require('jsdoc/augment');
var borrow = require('jsdoc/borrow');
var Package = require('jsdoc/package').Package;
var docs;
var packageDocs;
props.docs = docs = app.jsdoc.parser.parse(env.sourceFiles, env.opts.encoding);
// If there is no package.json, just create an empty package
packageDocs = new Package(props.packageJson);
packageDocs.files = env.sourceFiles || [];
docs.push(packageDocs);
logger.debug('Adding inherited symbols...');
borrow.indexAll(docs);
augment.addInherited(docs);
augment.addImplemented(docs);
borrow.resolveBorrows(docs);
app.jsdoc.parser.fireProcessingComplete(docs);
return cli;
};
cli.processParseResults = function() {
if (env.opts.explain) {
cli.dumpParseResults();
}
else {
cli.resolveTutorials();
cli.generateDocs();
}
return cli;
};
cli.dumpParseResults = function() {
global.dump(props.docs);
return cli;
};
cli.resolveTutorials = function() {
var resolver = require('jsdoc/tutorial/resolver');
if (env.opts.tutorials) {
resolver.load(env.opts.tutorials);
resolver.resolve();
}
return cli;
};
cli.generateDocs = function() {
var path = require('jsdoc/path');
var resolver = require('jsdoc/tutorial/resolver');
var taffy = require('taffydb').taffy;
var template;
env.opts.template = (function() {
var publish = env.opts.template || 'templates/default';
var templatePath = path.getResourcePath(publish);
// if we didn't find the template, keep the user-specified value so the error message is
// useful
return templatePath || env.opts.template;
})();
try {
template = require(env.opts.template + '/publish');
}
catch(e) {
logger.fatal('Unable to load template: ' + e.message || e);
}
// templates should include a publish.js file that exports a "publish" function
if (template.publish && typeof template.publish === 'function') {
logger.printInfo('Generating output files...');
template.publish(
taffy(props.docs),
env.opts,
resolver.root
);
logger.info('complete.');
}
else {
logger.fatal(env.opts.template + ' does not export a "publish" function. Global ' +
'"publish" functions are no longer supported.');
}
return cli;
};
// TODO: docs
cli.exit = function(exitCode, message) {
if (message && exitCode > 0) {
console.error(message);
}
process.exit(exitCode || 0);
};
return cli;
})();
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | 2 2 2 2 2 2 2 1 | //#!/usr/bin/env node
/*global arguments, require: true */
/**
* @project jsdoc
* @author Michael Mathews <micmath@gmail.com>
* @license See LICENSE.md file included in this distribution.
*/
/**
* Data representing the environment in which this app is running.
*
* @namespace
* @name env
*/
global.env = {
/**
* Running start and finish times.
*
* @memberof env
*/
run: {
start: new Date(),
finish: null
},
/**
* The command-line arguments passed into JSDoc.
*
* @type Array
* @memberof env
*/
args: [],
/**
* The parsed JSON data from the configuration file.
*
* @type Object
* @memberof env
*/
conf: {},
/**
* The absolute path to the base directory of the JSDoc application.
*
* @private
* @type string
* @memberof env
*/
dirname: '.',
/**
* The user's working directory at the time that JSDoc was started.
*
* @private
* @type string
* @memberof env
*/
pwd: null,
/**
* The command-line options, parsed into a key/value hash.
*
* @type Object
* @memberof env
* @example if (global.env.opts.help) { console.log('Helpful message.'); }
*/
opts: {},
/**
* The source files that JSDoc will parse.
* @type Array
* @memberof env
*/
sourceFiles: [],
/**
* The JSDoc version number and revision date.
*
* @type Object
* @memberof env
*/
version: {}
};
// initialize the environment for the current JavaScript VM
(function(args) {
'use strict';
var path;
Eif (args[0] && typeof args[0] === 'object') {
// we should be on Node.js
args = [__dirname, process.cwd()];
path = require('path');
// Create a custom require method that adds `lib/jsdoc` and `node_modules` to the module
// lookup path. This makes it possible to `require('jsdoc/foo')` from external templates and
// plugins, and within JSDoc itself. It also allows external templates and plugins to
// require JSDoc's module dependencies without installing them locally.
require = require('requizzle')({
requirePaths: {
before: [path.join(__dirname, 'lib')],
after: [path.join(__dirname, 'node_modules')]
},
infect: true
});
}
require('./lib/jsdoc/util/runtime').initialize(args);
})( Array.prototype.slice.call(arguments, 0) );
/**
* Data that must be shared across the entire application.
*
* @namespace
* @name app
*/
global.app = {
jsdoc: {
name: require('./lib/jsdoc/name'),
parser: null,
scanner: new (require('./lib/jsdoc/src/scanner').Scanner)()
}
};
/**
* Recursively print an object's properties to stdout. This method is safe to use with objects that
* contain circular references. In addition, on Mozilla Rhino, this method is safe to use with
* native Java objects.
*
* @global
* @name dump
* @private
* @param {Object} obj - Object(s) to print to stdout.
*/
global.dump = function() {
'use strict';
var doop = require('./lib/jsdoc/util/doop').doop;
var _dump = require('./lib/jsdoc/util/dumper').dump;
for (var i = 0, l = arguments.length; i < l; i++) {
console.log( _dump(doop(arguments[i])) );
}
};
(function() {
'use strict';
var logger = require('./lib/jsdoc/util/logger');
var runtime = require('./lib/jsdoc/util/runtime');
var cli = require('./cli');
function cb(errorCode) {
cli.logFinish();
cli.exit(errorCode || 0);
}
cli.setVersionInfo()
.loadConfig();
if (!global.env.opts.test) {
cli.configureLogger();
}
cli.logStart();
// On Rhino, we use a try/catch block so we can log the Java exception (if available)
if ( runtime.isRhino() ) {
try {
cli.runCommand(cb);
}
catch(e) {
if (e.rhinoException) {
logger.fatal( e.rhinoException.printStackTrace() );
} else {
console.trace(e);
cli.exit(1);
}
}
}
else {
cli.runCommand(cb);
}
})();
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| fs.js | 11.11% | (4 / 36) | 0% | (0 / 6) | 0% | (0 / 4) | 11.11% | (4 / 36) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | 2 2 2 2 | 'use strict';
var fs = require('fs');
var path = require('path');
var stream = require('stream');
var wrench = require('wrench');
var toDir = exports.toDir = function(_path) {
var isDirectory;
_path = path.normalize(_path);
try {
isDirectory = fs.statSync(_path).isDirectory();
}
catch(e) {
isDirectory = false;
}
if (isDirectory) {
return _path;
} else {
return path.dirname(_path);
}
};
exports.mkPath = function(/**Array*/ _path) {
if ( Array.isArray(_path) ) {
_path = _path.join('');
}
wrench.mkdirSyncRecursive(_path);
};
// adapted from http://procbits.com/2011/11/15/synchronous-file-copy-in-node-js
exports.copyFileSync = function(inFile, outDir, fileName) {
var BUF_LENGTH = 64 * 1024;
var read;
var write;
var buffer = new Buffer(BUF_LENGTH);
var bytesRead = 1;
var outFile = path.join( outDir, fileName || path.basename(inFile) );
var pos = 0;
wrench.mkdirSyncRecursive(outDir);
read = fs.openSync(inFile, 'r');
write = fs.openSync(outFile, 'w');
while (bytesRead > 0) {
bytesRead = fs.readSync(read, buffer, 0, BUF_LENGTH, pos);
fs.writeSync(write, buffer, 0, bytesRead);
pos += bytesRead;
}
fs.closeSync(read);
return fs.closeSync(write);
};
Object.keys(fs).forEach(function(key) {
exports[key] = fs[key];
});
|
| File | Statements | Branches | Functions | Lines | |||||
|---|---|---|---|---|---|---|---|---|---|
| commentConvert.js | 25% | (1 / 4) | 100% | (0 / 0) | 0% | (0 / 2) | 25% | (1 / 4) | |
| commentsOnly.js | 25% | (1 / 4) | 0% | (0 / 2) | 0% | (0 / 1) | 25% | (1 / 4) | |
| escapeHtml.js | 33.33% | (1 / 3) | 0% | (0 / 2) | 0% | (0 / 1) | 33.33% | (1 / 3) | |
| eventDumper.js | 10.81% | (4 / 37) | 0% | (0 / 35) | 0% | (0 / 6) | 10.81% | (4 / 37) | |
| markdown.js | 16.67% | (6 / 36) | 7.69% | (2 / 26) | 0% | (0 / 6) | 16.67% | (6 / 36) | |
| overloadHelper.js | 12.86% | (9 / 70) | 0% | (0 / 30) | 0% | (0 / 17) | 12.86% | (9 / 70) | |
| partial.js | 12.5% | (1 / 8) | 100% | (0 / 0) | 0% | (0 / 2) | 12.5% | (1 / 8) | |
| railsTemplate.js | 33.33% | (1 / 3) | 0% | (0 / 2) | 0% | (0 / 1) | 33.33% | (1 / 3) | |
| shout.js | 33.33% | (1 / 3) | 0% | (0 / 2) | 0% | (0 / 1) | 33.33% | (1 / 3) | |
| sourcetag.js | 6.67% | (1 / 15) | 0% | (0 / 10) | 0% | (0 / 2) | 6.67% | (1 / 15) | |
| summarize.js | 5% | (1 / 20) | 0% | (0 / 11) | 0% | (0 / 2) | 5% | (1 / 20) |
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 1 | /**
@overview Demonstrate how to modify the source code before the parser sees it.
@module plugins/commentConvert
@author Michael Mathews <micmath@gmail.com>
*/
'use strict';
/*eslint spaced-line-comment: 0 */
exports.handlers = {
///
/// Convert ///-style comments into jsdoc comments.
/// @param e
/// @param e.filename
/// @param e.source
///
beforeParse: function(e) {
e.source = e.source.replace(/(\n[ \t]*\/\/\/[^\n]*)+/g, function($) {
var replacement = '\n/**' + $.replace(/^[ \t]*\/\/\//mg, '').replace(/(\n$|$)/, '*/$1');
return replacement;
});
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 1 | /**
* @overview Remove everything in a file except JSDoc-style comments. By enabling this plugin, you
* can document source files that are not valid JavaScript (including source files for other
* languages).
* @module plugins/commentsOnly
* @author Jeff Williams <jeffrey.l.williams@gmail.com>
*/
'use strict';
exports.handlers = {
beforeParse: function(e) {
// a JSDoc comment looks like: /**[one or more chars]*/
var comments = e.source.match(/\/\*\*[\s\S]+?\*\//g);
if (comments) {
e.source = comments.join('\n\n');
}
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 1 | /**
@overview Escape HTML tags in descriptions.
@module plugins/escapeHtml
@author Michael Mathews <micmath@gmail.com>
*/
'use strict';
exports.handlers = {
/**
Translate HTML tags in descriptions into safe entities.
Replaces <, & and newlines
*/
newDoclet: function(e) {
if (e.doclet.description) {
e.doclet.description = e.doclet.description
.replace(/&/g,'&')
.replace(/</g,'<')
.replace(/\r\n|\n|\r/g, '<br>');
}
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | 2 1 1 1 | /*global env: true */
/**
* @overview Dump information about parser events to the console.
* @module plugins/eventDumper
* @author Jeff Williams <jeffrey.l.williams@gmail.com>
*/
'use strict';
var _ = require('underscore');
var util = require('util');
var conf = env.conf.eventDumper || {};
var isRhino = require('jsdoc/util/runtime').isRhino();
// Dump the included parser events (defaults to all events)
var events = conf.include || [
'parseBegin',
'fileBegin',
'beforeParse',
'jsdocCommentFound',
'symbolFound',
'newDoclet',
'fileComplete',
'parseComplete',
'processingComplete'
];
// Don't dump the excluded parser events
if (conf.exclude) {
events = _.difference(events, conf.exclude);
}
/**
* Check whether a variable appears to be a Java native object.
*
* @param {*} o - The variable to check.
* @return {boolean} Set to `true` for Java native objects and `false` in all other cases.
*/
function isJavaNativeObject(o) {
if (!isRhino) {
return false;
}
return o && typeof o === 'object' && typeof o.getClass === 'function';
}
/**
* Replace AST node objects in events with a placeholder.
*
* @param {Object} o - An object whose properties may contain AST node objects.
* @return {Object} The modified object.
*/
function replaceNodeObjects(o) {
var doop = require('jsdoc/util/doop');
var OBJECT_PLACEHOLDER = '<Object>';
if (o.code && o.code.node) {
// don't break the original object!
o.code = doop(o.code);
o.code.node = OBJECT_PLACEHOLDER;
}
if (o.doclet && o.doclet.meta && o.doclet.meta.code && o.doclet.meta.code.node) {
// don't break the original object!
o.doclet.meta.code = doop(o.doclet.meta.code);
o.doclet.meta.code.node = OBJECT_PLACEHOLDER;
}
if (o.astnode) {
o.astnode = OBJECT_PLACEHOLDER;
}
return o;
}
/**
* Get rid of unwanted crud in an event object.
*
* @param {object} e The event object.
* @return {object} The fixed-up object.
*/
function cleanse(e) {
var result = {};
Object.keys(e).forEach(function(prop) {
// by default, don't stringify properties that contain an array of functions
if (!conf.includeFunctions && util.isArray(e[prop]) && e[prop][0] &&
String(typeof e[prop][0]) === 'function') {
result[prop] = 'function[' + e[prop].length + ']';
}
// never include functions that belong to the object
else if (typeof e[prop] !== 'function') {
// don't call JSON.stringify() on Java native objects--Rhino will throw an exception
result[prop] = isJavaNativeObject(e[prop]) ? String(e[prop]) : e[prop];
}
});
// allow users to omit node objects, which can be enormous
if (conf.omitNodes) {
result = replaceNodeObjects(result);
}
return result;
}
exports.handlers = {};
events.forEach(function(eventType) {
exports.handlers[eventType] = function(e) {
console.log( JSON.stringify({
type: eventType,
content: cleanse(e)
}, null, 4) );
};
});
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | 2 2 2 2 1 1 | /**
* @overview Translate doclet descriptions from MarkDown into HTML.
* @module plugins/markdown
* @author Michael Mathews <micmath@gmail.com>
* @author Ben Blank <ben.blank@gmail.com>
*/
'use strict';
var config = global.env.conf.markdown || {};
var defaultTags = [
'classdesc',
'description',
'exceptions',
'params',
'properties',
'returns',
'see'
];
var hasOwnProp = Object.prototype.hasOwnProperty;
var parse = require('jsdoc/util/markdown').getParser();
var tags = [];
var excludeTags = [];
function shouldProcessString(tagName, text) {
var shouldProcess = false;
if (tagName !== 'see') {
shouldProcess = true;
}
// we only want to process `@see` tags that contain Markdown links
else if (tagName === 'see' && text.indexOf('[') !== -1) {
shouldProcess = true;
}
return shouldProcess;
}
/**
* Process the markdown source in a doclet. The properties that should be
* processed are configurable, but always include "classdesc", "description",
* "params", "properties", and "returns". Handled properties can be bare
* strings, objects, or arrays of objects.
*/
function process(doclet) {
tags.forEach(function(tag) {
if ( !hasOwnProp.call(doclet, tag) ) {
return;
}
if (typeof doclet[tag] === 'string' && shouldProcessString(tag, doclet[tag]) ) {
doclet[tag] = parse(doclet[tag]);
}
else if ( Array.isArray(doclet[tag]) ) {
doclet[tag].forEach(function(value, index, original) {
var inner = {};
inner[tag] = value;
process(inner);
original[index] = inner[tag];
});
}
else if (doclet[tag]) {
process(doclet[tag]);
}
});
}
// set up the list of "tags" (properties) to process
if (config.tags) {
tags = config.tags.slice();
}
// set up the list of default tags to exclude from processing
if (config.excludeTags) {
excludeTags = config.excludeTags.slice();
}
defaultTags.forEach(function(tag) {
if (excludeTags.indexOf(tag) === -1 && tags.indexOf(tag) === -1) {
tags.push(tag);
}
});
exports.handlers = {
/**
* Translate markdown syntax in a new doclet's description into HTML. Is run
* by JSDoc 3 whenever a "newDoclet" event fires.
*/
newDoclet: function(e) {
process(e.doclet);
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | 1 1 1 1 1 1 1 1 1 | /**
* The Overload Helper plugin automatically adds a signature-like string to the longnames of
* overloaded functions and methods. In JSDoc, this string is known as a _variation_. (The longnames
* of overloaded constructor functions are _not_ updated, so that JSDoc can identify the class'
* members correctly.)
*
* Using this plugin allows you to link to overloaded functions without manually adding `@variation`
* tags to your documentation.
*
* For example, suppose your code includes a function named `foo` that you can call in the
* following ways:
*
* + `foo()`
* + `foo(bar)`
* + `foo(bar, baz)` (where `baz` is repeatable)
*
* This plugin assigns the following variations and longnames to each version of `foo`:
*
* + `foo()` gets the variation `()` and the longname `foo()`.
* + `foo(bar)` gets the variation `(bar)` and the longname `foo(bar)`.
* + `foo(bar, baz)` (where `baz` is repeatable) gets the variation `(bar, ...baz)` and the longname
* `foo(bar, ...baz)`.
*
* You can then link to these functions with `{@link foo()}`, `{@link foo(bar)}`, and
* `{@link foo(bar, ...baz)`. Note that the variation is based on the names of the function
* parameters, _not_ their types.
*
* If you prefer to manually assign variations to certain functions, you can still do so with the
* `@variation` tag. This plugin will not change these variations or add more variations for that
* function, as long as the variations you've defined result in unique longnames.
*
* If an overloaded function includes multiple signatures with the same parameter names, the plugin
* will assign numeric variations instead, starting at `(1)` and counting upwards.
*
* @module plugins/overloadHelper
* @author Jeff Williams <jeffrey.l.williams@gmail.com>
* @license Apache License 2.0
*/
'use strict';
// lookup table of function doclets by longname
var functionDoclets;
function hasUniqueValues(obj) {
var isUnique = true;
var seen = [];
Object.keys(obj).forEach(function(key) {
if (seen.indexOf(obj[key]) !== -1) {
isUnique = false;
}
seen.push(obj[key]);
});
return isUnique;
}
function getParamNames(params) {
var names = [];
params.forEach(function(param) {
var name = param.name || '';
if (param.variable) {
name = '...' + name;
}
if (name !== '') {
names.push(name);
}
});
return names.length ? names.join(', ') : '';
}
function getParamVariation(doclet) {
return getParamNames(doclet.params || []);
}
function getUniqueVariations(doclets) {
var counter = 0;
var variations = {};
var docletKeys = Object.keys(doclets);
function getUniqueNumbers() {
var format = require('util').format;
docletKeys.forEach(function(doclet) {
var newLongname;
while (true) {
counter++;
variations[doclet] = String(counter);
// is this longname + variation unique?
newLongname = format('%s(%s)', doclets[doclet].longname, variations[doclet]);
if ( !functionDoclets[newLongname] ) {
break;
}
}
});
}
function getUniqueNames() {
// start by trying to preserve existing variations
docletKeys.forEach(function(doclet) {
variations[doclet] = doclets[doclet].variation || getParamVariation(doclets[doclet]);
});
// if they're identical, try again, without preserving existing variations
if ( !hasUniqueValues(variations) ) {
docletKeys.forEach(function(doclet) {
variations[doclet] = getParamVariation(doclets[doclet]);
});
// if they're STILL identical, switch to numeric variations
if ( !hasUniqueValues(variations) ) {
getUniqueNumbers();
}
}
}
// are we already using numeric variations? if so, keep doing that
if (functionDoclets[doclets.newDoclet.longname + '(1)']) {
getUniqueNumbers();
}
else {
getUniqueNames();
}
return variations;
}
function ensureUniqueLongname(newDoclet) {
var doclets = {
oldDoclet: functionDoclets[newDoclet.longname],
newDoclet: newDoclet
};
var docletKeys = Object.keys(doclets);
var oldDocletLongname;
var variations = {};
if (doclets.oldDoclet) {
oldDocletLongname = doclets.oldDoclet.longname;
// if the shared longname has a variation, like MyClass#myLongname(variation),
// remove the variation
if (doclets.oldDoclet.variation || doclets.oldDoclet.variation === '') {
docletKeys.forEach(function(doclet) {
doclets[doclet].longname = doclets[doclet].longname.replace(/\([\s\S]*\)$/, '');
doclets[doclet].variation = null;
});
}
variations = getUniqueVariations(doclets);
// update the longnames/variations
docletKeys.forEach(function(doclet) {
doclets[doclet].longname += '(' + variations[doclet] + ')';
doclets[doclet].variation = variations[doclet];
});
// update the old doclet in the lookup table
functionDoclets[oldDocletLongname] = null;
functionDoclets[doclets.oldDoclet.longname] = doclets.oldDoclet;
}
// always store the new doclet in the lookup table
functionDoclets[doclets.newDoclet.longname] = doclets.newDoclet;
return doclets.newDoclet;
}
exports.handlers = {
parseBegin: function() {
functionDoclets = {};
},
newDoclet: function(e) {
if (e.doclet.kind === 'function') {
e.doclet = ensureUniqueLongname(e.doclet);
}
},
parseComplete: function() {
functionDoclets = null;
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 2 | /**
@overview Adds support for reusable partial jsdoc files.
@module plugins/partial
@author Ludo Antonov <ludo@hulu.com>
*/
'use strict';
var fs = require('jsdoc/fs');
var path = require('path');
exports.handlers = {
/**
* Include a partial jsdoc
*
* @param e
* @param e.filename
* @param e.source
* @example
* @partial "partial_doc.jsdoc"
*/
beforeParse: function(e) {
e.source = e.source.replace(/(@partial \".*\")+/g, function($) {
var pathArg = $.match(/\".*\"/)[0].replace(/"/g,'');
var fullPath = path.join(e.filename , '..', pathArg);
var partialData = fs.readFileSync(fullPath, global.env.opts.encoding);
return partialData;
});
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 1 | /**
@overview Strips the rails template tags from a js.erb file
@module plugins/railsTemplate
@author Jannon Frank <jannon@jannon.net>
*/
'use strict';
exports.handlers = {
/**
* Remove rails tags from the source input (e.g. <% foo bar %>)
* @param e
* @param e.filename
* @param e.source
*/
beforeParse: function(e) {
if (e.filename.match(/\.erb$/)) {
e.source = e.source.replace(/<%.*%>/g, '');
}
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 1 | /**
@overview This is just an example.
@module plugins/shout
@author Michael Mathews <micmath@gmail.com>
*/
'use strict';
exports.handlers = {
/**
Make your descriptions more shoutier.
*/
newDoclet: function(e) {
if (typeof e.doclet.description === 'string') {
e.doclet.description = e.doclet.description.toUpperCase();
}
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | 2 | /**
@module plugins/sourcetag
@author Michael Mathews <micmath@gmail.com>
*/
'use strict';
var logger = require('jsdoc/util/logger');
exports.handlers = {
/**
Support @source tag. Expected value like:
{ "filename": "myfile.js", "lineno": 123 }
Modifies the corresponding meta values on the given doclet.
WARNING: If you are using a JSDoc template that generates pretty-printed source files,
such as JSDoc's default template, this plugin can cause JSDoc to crash. To fix this issue,
update your template settings to disable pretty-printed source files.
@source { "filename": "sourcetag.js", "lineno": 13 }
*/
newDoclet: function(e) {
var tags = e.doclet.tags,
tag,
value;
// any user-defined tags in this doclet?
if (typeof tags !== 'undefined') {
// only interested in the @source tags
tags = tags.filter(function($) {
return $.title === 'source';
});
if (tags.length) {
// take the first one
tag = tags[0];
try {
value = JSON.parse(tag.value);
}
catch(e) {
logger.error('@source tag expects a valid JSON value, like { "filename": "myfile.js", "lineno": 123 }.');
return;
}
e.doclet.meta = e.doclet.meta || {};
e.doclet.meta.filename = value.filename || '';
e.doclet.meta.lineno = value.lineno || '';
}
}
}
};
|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | 1 | /**
* @overview This plugin creates a summary tag, if missing, from the first sentence in the
* description.
* @module plugins/summarize
* @author Mads Bondo Dydensborg <mbd@dbc.dk>
*/
'use strict';
exports.handlers = {
/**
* Autogenerate summaries, if missing, from the description, if present.
*/
newDoclet: function(e) {
var endTag;
var tags;
var stack;
// If the summary is missing, grab the first sentence from the description
// and use that.
if (e.doclet && !e.doclet.summary && e.doclet.description) {
// The summary may end with `.$`, `. `, or `.<` (a period followed by an HTML tag).
e.doclet.summary = e.doclet.description.split(/\.$|\.\s|\.</)[0];
// Append `.` as it was removed in both cases, or is possibly missing.
e.doclet.summary += '.';
// This is an excerpt of something that is possibly HTML.
// Balance it using a stack. Assume it was initially balanced.
tags = e.doclet.summary.match(/<[^>]+>/g) || [];
stack = [];
tags.forEach(function(tag) {
var idx = tag.indexOf('/');
if (idx === -1) {
// start tag -- push onto the stack
stack.push(tag);
} else if (idx === 1) {
// end tag -- pop off of the stack
stack.pop();
}
// otherwise, it's a self-closing tag; don't modify the stack
});
// stack should now contain only the start tags that lack end tags,
// with the most deeply nested start tag at the top
while (stack.length > 0) {
// pop the unmatched tag off the stack
endTag = stack.pop();
// get just the tag name
endTag = endTag.substring(1, endTag.search(/[ >]/));
// append the end tag
e.doclet.summary += '</' + endTag + '>';
}
// and, finally, if the summary starts and ends with a <p> tag, remove it; let the
// template decide whether to wrap the summary in a <p> tag
e.doclet.summary = e.doclet.summary.replace(/^<p>(.*)<\/p>$/i, '$1');
}
}
};
|